2009-08-13 40 views
11

Giả sử tôi có mã sau cập nhật trường của phản chiếu sử dụng struct. Vì cá thể struct được sao chép vào phương thức DynamicUpdate, it needs to be boxed to an object before being passed.Tạo phương thức động để đặt trường của cấu trúc thay vì sử dụng phản chiếu

struct Person 
{ 
    public int id; 
} 

class Test 
{ 
    static void Main() 
    { 
     object person = RuntimeHelpers.GetObjectValue(new Person()); 
     DynamicUpdate(person); 
     Console.WriteLine(((Person)person).id); // print 10 
    } 

    private static void DynamicUpdate(object o) 
    { 
     FieldInfo field = typeof(Person).GetField("id"); 
     field.SetValue(o, 10); 
    } 
} 

Mã hoạt động tốt. Bây giờ, giả sử tôi không muốn sử dụng sự phản chiếu vì nó chậm. Thay vào đó, tôi muốn tạo một số CIL trực tiếp sửa đổi trường id và chuyển đổi CIL đó thành một đại biểu có thể tái sử dụng (ví dụ: sử dụng tính năng Phương thức động). Đặc biệt, tôi muốn thay thế mã trên với s/t như thế này:

static void Main() 
{ 
    var action = CreateSetIdDelegate(typeof(Person)); 
    object person = RuntimeHelpers.GetObjectValue(new Person()); 
    action(person, 10); 
    Console.WriteLine(((Person)person).id); // print 10 
} 

private static Action<object, object> CreateSetIdDelegate(Type t) 
{ 
    // build dynamic method and return delegate 
}  

Câu hỏi của tôi: là có cách nào để thực hiện CreateSetIdDelegate excepts từ bằng một trong những kỹ thuật sau đây?

  1. Tạo CIL gọi trình setter sử dụng phản chiếu (làm phân đoạn mã đầu tiên trong bài đăng này). Điều này không có ý nghĩa, vì yêu cầu là để loại bỏ sự phản chiếu, nhưng đó là một sự thực hiện có thể vì vậy tôi chỉ đề cập đến.
  2. Thay vì sử dụng Action<object, object>, hãy sử dụng đại biểu tùy chỉnh có chữ ký là public delegate void Setter(ref object target, object value).
  3. Thay vì sử dụng Action<object, object>, hãy sử dụng Action<object[], object> với phần tử thứ nhất của mảng là đối tượng mục tiêu.

Lý do tôi không thích 2 & 3 là bởi vì tôi không muốn có các đại biểu khác nhau cho các setter của đối tượng và setter của struct (cũng như không muốn làm cho các thiết lập đối tượng trường ủy nhiệm phức tạp hơn cần thiết, ví dụ: Action<object, object>). Tôi nghĩ rằng việc thực hiện CreateSetIdDelegate sẽ tạo ra CIL khác nhau tùy thuộc vào loại mục tiêu là cấu trúc hay đối tượng, nhưng tôi muốn nó trả về cùng một đại biểu cung cấp cùng một API cho người dùng.

+2

là sử dụng một struct * có thể thay đổi thực sự * lựa chọn tốt nhất của bạn ở đây? Nó gần như luôn luôn là một nỗi đau vì nhiều lý do, và có vẻ như bạn đang chạy vào một số trong số họ ... –

+1

Bạn đã xem xét biên dịch một cây biểu thức thay vì phát ra IL? Nó sẽ dễ dàng hơn nhiều. –

+0

@Jon: thực sự tôi đang xây dựng một API phản chiếu nhanh (http://fasterflect.codeplex.com/) để hỗ trợ cho các hoạt động phản chiếu cấu trúc sẽ là mong muốn của một số người. –

Trả lời

14

EDIT lại: Đây công trình xây dựng bây giờ.

Có một cách tuyệt vời để làm điều đó trong C# 4, nhưng bạn sẽ phải tự viết mã số phát minh của riêng mình trước khi sử dụng ILGenerator. Họ nói thêm một ExpressionType.Assign đến .NET Framework 4.

này hoạt động trong C# 4 (thử nghiệm):

public delegate void ByRefStructAction(ref SomeType instance, object value); 

private static ByRefStructAction BuildSetter(FieldInfo field) 
{ 
    ParameterExpression instance = Expression.Parameter(typeof(SomeType).MakeByRefType(), "instance"); 
    ParameterExpression value = Expression.Parameter(typeof(object), "value"); 

    Expression<ByRefStructAction> expr = 
     Expression.Lambda<ByRefStructAction>(
      Expression.Assign(
       Expression.Field(instance, field), 
       Expression.Convert(value, field.FieldType)), 
      instance, 
      value); 

    return expr.Compile(); 
} 

Edit: Đây là mã kiểm tra của tôi.

public struct SomeType 
{ 
    public int member; 
} 

[TestMethod] 
public void TestIL() 
{ 
    FieldInfo field = typeof(SomeType).GetField("member"); 
    var setter = BuildSetter(field); 
    SomeType instance = new SomeType(); 
    int value = 12; 
    setter(ref instance, value); 
    Assert.AreEqual(value, instance.member); 
} 
+0

Sử dụng tốt câu lệnh ET của C# 4. +1. Nhưng việc sử dụng * ref * là s/t tôi muốn tránh (xem viên đạn thứ 2 trong câu hỏi của tôi) bởi vì tôi không muốn xây dựng các đại biểu riêng biệt cho struct setter & class setter. –

+5

Đối với một cấu trúc, bạn phải chuyển nó bằng ref để thay đổi bản gốc. Vì bạn không thể thêm 'ref' hoặc' out' vào các tác nhân 'Action <>', bạn có các tùy chọn sau: 1) Sử dụng một lớp thay vì struct, 2) Sử dụng các đại biểu tùy chỉnh, hoặc 3) không phải thay đổi cấu trúc và chuyển cấu trúc theo giá trị [đã đóng hộp]. :) –

1

Bạn có thể muốn có một cái nhìn tại các phương pháp năng động (phản ánh không phải là chậm!) ...

Gerhard có một bài thoải mái về điều đó: http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/

+0

Đó là những gì tôi đang làm (và hỏi trong câu hỏi này); Tôi muốn thay thế sự phản chiếu bằng cách sử dụng phương thức động và câu hỏi sẽ hỏi liệu tôi có thể chia sẻ cùng một API tạo đại biểu cho cả hai cấu trúc và các lớp. –

10

Tôi gặp phải sự cố tương tự và tôi mất nhiều nhất vào cuối tuần nhưng cuối cùng tôi đã tìm ra sau khi tìm kiếm, đọc và tháo các dự án thử nghiệm C#. Và phiên bản này chỉ yêu cầu .NET 2, không 4.

public delegate void SetterDelegate(ref object target, object value); 
private static Type[] ParamTypes = new Type[] 
{ 
    typeof(object).MakeByRefType(), typeof(object) 
}; 
private static SetterDelegate CreateSetMethod(MemberInfo memberInfo) 
{ 
    Type ParamType; 
    if (memberInfo is PropertyInfo) 
     ParamType = ((PropertyInfo)memberInfo).PropertyType; 
    else if (memberInfo is FieldInfo) 
     ParamType = ((FieldInfo)memberInfo).FieldType; 
    else 
     throw new Exception("Can only create set methods for properties and fields."); 

    DynamicMethod setter = new DynamicMethod(
     "", 
     typeof(void), 
     ParamTypes, 
     memberInfo.ReflectedType.Module, 
     true); 
    ILGenerator generator = setter.GetILGenerator(); 
    generator.Emit(OpCodes.Ldarg_0); 
    generator.Emit(OpCodes.Ldind_Ref); 

    if (memberInfo.DeclaringType.IsValueType) 
    { 
#if UNSAFE_IL 
     generator.Emit(OpCodes.Unbox, memberInfo.DeclaringType); 
#else 
     generator.DeclareLocal(memberInfo.DeclaringType.MakeByRefType()); 
     generator.Emit(OpCodes.Unbox, memberInfo.DeclaringType); 
     generator.Emit(OpCodes.Stloc_0); 
     generator.Emit(OpCodes.Ldloc_0); 
#endif // UNSAFE_IL 
    } 

    generator.Emit(OpCodes.Ldarg_1); 
    if (ParamType.IsValueType) 
     generator.Emit(OpCodes.Unbox_Any, ParamType); 

    if (memberInfo is PropertyInfo) 
     generator.Emit(OpCodes.Callvirt, ((PropertyInfo)memberInfo).GetSetMethod()); 
    else if (memberInfo is FieldInfo) 
     generator.Emit(OpCodes.Stfld, (FieldInfo)memberInfo); 

    if (memberInfo.DeclaringType.IsValueType) 
    { 
#if !UNSAFE_IL 
     generator.Emit(OpCodes.Ldarg_0); 
     generator.Emit(OpCodes.Ldloc_0); 
     generator.Emit(OpCodes.Ldobj, memberInfo.DeclaringType); 
     generator.Emit(OpCodes.Box, memberInfo.DeclaringType); 
     generator.Emit(OpCodes.Stind_Ref); 
#endif // UNSAFE_IL 
    } 
    generator.Emit(OpCodes.Ret); 

    return (SetterDelegate)setter.CreateDelegate(typeof(SetterDelegate)); 
} 

Lưu ý công cụ "#if UNSAFE_IL" trong đó.Tôi thực sự đã đưa ra 2 cách để làm điều đó, nhưng cách đầu tiên thực sự là ... hackish. Để báo giá từ Ecma-335, tài liệu chuẩn cho IL:

"Không giống như hộp, được yêu cầu tạo bản sao của loại giá trị để sử dụng trong đối tượng, không cần phải bỏ hộp để sao chép loại giá trị từ đối tượng Thông thường nó đơn giản tính toán địa chỉ của kiểu giá trị đã có mặt bên trong của đối tượng được đóng hộp. " Vì vậy, nếu bạn muốn chơi nguy hiểm, bạn có thể sử dụng OpCodes.Unbox để thay đổi đối tượng xử lý của bạn thành một con trỏ đến cấu trúc của bạn, sau đó có thể được sử dụng như tham số đầu tiên của một Stfld hoặc Callvirt. Làm theo cách này thực sự kết thúc việc thay đổi cấu trúc tại chỗ, và bạn thậm chí không cần truyền đối tượng đích của mình bằng ref.

Tuy nhiên, lưu ý rằng tiêu chuẩn không đảm bảo rằng Unbox sẽ cung cấp cho bạn một con trỏ tới phiên bản được đóng hộp. Đặc biệt, nó cho thấy rằng Nullable <> có thể gây ra Unbox để tạo một bản sao. Dù sao, nếu điều đó xảy ra, bạn có thể sẽ nhận được một sự im lặng thất bại, nơi nó đặt giá trị trường hoặc thuộc tính trên một bản sao cục bộ mà sau đó bị loại bỏ ngay lập tức. Vì vậy, cách an toàn để làm điều đó là vượt qua đối tượng của bạn bằng cách ref, lưu trữ địa chỉ trong một biến địa phương, thực hiện sửa đổi, và sau đó rebox kết quả và đặt nó trở lại trong tham số đối tượng ByRef của bạn.

tôi đã làm một số timings thô, kêu gọi mỗi phiên bản 10.000.000 lần, với 2 cấu trúc khác nhau:

Cấu trúc với 1 lĩnh vực: 0,46 s "không an toàn" đại biểu .70 s "an toàn" đại biểu 4,5 s FieldInfo .SetValue

cấu với 4 lĩnh vực: 0,46 s "không an toàn" đại biểu .88 s "an toàn" đại biểu 4,5 s FieldInfo.SetValue

ý rằng boxing làm cho thứ Tốc độ phiên bản "An toàn" giảm với kích thước cấu trúc, trong khi hai phương pháp khác không bị ảnh hưởng bởi kích thước cấu trúc. Tôi đoán tại một số điểm chi phí đấm bốc sẽ vượt qua chi phí phản ánh. Nhưng tôi không tin vào phiên bản "Không an toàn" trong bất kỳ khả năng quan trọng nào.

+0

Câu trả lời hay. Đối với vấn đề cụ thể của tôi (tức là để thực hiện thư viện của tôi http://fasterflect.codeplex.com), tôi không muốn sử dụng 'ref'. Thay vào đó, tôi yêu cầu mã gọi để chuyển vào một số trình bao bọc cấu trúc, đó là một kiểu giá trị. –

+0

Nó sẽ không cho phép tôi bình luận về câu trả lời của Hugh dưới đây, vì vậy tôi sẽ trả lời ở đây. Về cơ bản để thiết lập một lĩnh vực trên một cấu trúc, bạn phải hộp cấu trúc đầu tiên, như thế này: object x = new MyStruct(); Sau đó, bạn vượt qua đối tượng đóng hộp trong ref. Nếu bạn đã biết loại bạn đang xử lý trước, bạn không cần mã này chút nào. Và nếu bạn không biết loại đó là gì, điều đó có nghĩa là nó có thể là một tham chiếu đối tượng rồi. Hy vọng rằng có ý nghĩa. –

+0

@BryceWagner có thể thực hiện điều này với thông số được biết tại thời gian chạy không? Sẽ thay đổi 'typeof (object) .MakeByRefType(), typeof (object)' thành 'typeof (knownType) .MakeByRefType(), typeof (knownFieldType)' có đủ không? Tôi chỉ không biết phần phát ra và vẫn không thể sử dụng .net 4.0. – AgentFire

4

Sau khi một số thí nghiệm:

public delegate void ClassFieldSetter<in T, in TValue>(T target, TValue value) where T : class; 

public delegate void StructFieldSetter<T, in TValue>(ref T target, TValue value) where T : struct; 

public static class FieldSetterCreator 
{ 
    public static ClassFieldSetter<T, TValue> CreateClassFieldSetter<T, TValue>(FieldInfo field) 
     where T : class 
    { 
     return CreateSetter<T, TValue, ClassFieldSetter<T, TValue>>(field); 
    } 

    public static StructFieldSetter<T, TValue> CreateStructFieldSetter<T, TValue>(FieldInfo field) 
     where T : struct 
    { 
     return CreateSetter<T, TValue, StructFieldSetter<T, TValue>>(field); 
    } 

    private static TDelegate CreateSetter<T, TValue, TDelegate>(FieldInfo field) 
    { 
     return (TDelegate)(object)CreateSetter(field, typeof(T), typeof(TValue), typeof(TDelegate)); 
    } 

    private static Delegate CreateSetter(FieldInfo field, Type instanceType, Type valueType, Type delegateType) 
    { 
     if (!field.DeclaringType.IsAssignableFrom(instanceType)) 
      throw new ArgumentException("The field is declared it different type"); 
     if (!field.FieldType.IsAssignableFrom(valueType)) 
      throw new ArgumentException("The field type is not assignable from the value"); 

     var paramType = instanceType.IsValueType ? instanceType.MakeByRefType() : instanceType; 
     var setter = new DynamicMethod("", typeof(void), 
             new[] { paramType, valueType }, 
             field.DeclaringType.Module, true); 

     var generator = setter.GetILGenerator(); 
     generator.Emit(OpCodes.Ldarg_0); 
     generator.Emit(OpCodes.Ldarg_1); 
     generator.Emit(OpCodes.Stfld, field); 
     generator.Emit(OpCodes.Ret); 

     return setter.CreateDelegate(delegateType); 
    } 
} 

Sự khác biệt chính từ cách tiếp cận cây biểu hiện là lĩnh vực readonly cũng có thể được thay đổi.

+0

Câu trả lời hay nhất! – AgentFire

2

Mã này làm việc cho cấu trúc mà không sử dụng ref:

private Action<object, object> CreateSetter(FieldInfo field) 
{ 
    var instance = Expression.Parameter(typeof(object)); 
    var value = Expression.Parameter(typeof(object)); 

    var body = 
     Expression.Block(typeof(void), 
      Expression.Assign(
       Expression.Field(
        Expression.Unbox(instance, field.DeclaringType), 
        field), 
       Expression.Convert(value, field.FieldType))); 

    return (Action<object, object>)Expression.Lambda(body, instance, value).Compile(); 
} 

Dưới đây là mã kiểm tra của tôi:

public struct MockStruct 
{ 
    public int[] Values; 
} 

[TestMethod] 
public void MyTestMethod() 
{ 
    var field = typeof(MockStruct).GetField(nameof(MockStruct.Values)); 
    var setter = CreateSetter(field); 
    object mock = new MockStruct(); //note the boxing here. 
    setter(mock, new[] { 1, 2, 3 }); 
    var result = ((MockStruct)mock).Values; 
    Assert.IsNotNull(result); 
    Assert.IsTrue(new[] { 1, 2, 3 }.SequenceEqual(result)); 
} 
Các vấn đề liên quan