11

假设我有以下代码更新struct使用反射的字段。由于结构实例被复制到DynamicUpdate方法中,因此在传递之前需要将其装箱到对象中

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);
    }
}

代码工作正常。现在,假设我不想使用反射,因为它很慢。相反,我想生成一些直接修改id字段的 CIL 并将该 CIL 转换为可重用的委托(例如,使用动态方法功能)。特别是,我想用 s/t 替换上面的代码,如下所示:

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
}    

我的问题:有没有办法CreateSetIdDelegate通过使用以下技术之一来实现例外?

  1. 生成使用反射调用设置器的 CIL(作为本文中的第一个代码段)。这是没有意义的,因为要求是摆脱反射,但这是一种可能的实现,所以我只提一下。
  2. 不要使用Action<object, object>,而是使用签名为 的自定义委托public delegate void Setter(ref object target, object value)
  3. 而不是使用Action<object, object>,使用Action<object[], object>数组的第一个元素作为目标对象。

我不喜欢 2 和 3 的原因是因为我不想为 object 的 setter 和 struct 的 setter 使用不同的委托(也不想让 set-object-field 委托比必要的更复杂,例如Action<object, object>)。我认为CreateSetIdDelegate根据目标类型是结构还是对象,实现会生成不同的 CIL,但我希望它返回向用户提供相同 API 的相同委托。

4

6 回答 6

14

再次编辑:现在可以使用结构。

在 C# 4 中有一种很棒的方法可以做到这一点,但在此之前,您必须为任何事情编写自己的ILGenerator发出代码。他们ExpressionType.Assign在 .NET Framework 4 中添加了一个。

这适用于 C# 4(已测试):

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();
}

编辑:这是我的测试代码。

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);
}
于 2009-08-14T07:21:49.940 回答
10

我遇到了类似的问题,花了我一个周末的大部分时间,但经过大量搜索、阅读和反汇编 C# 测试项目,我终于弄明白了。而且这个版本只需要 .NET 2,而不是 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));
}

请注意其中的“#if UNSAFE_IL”内容。我实际上想出了两种方法来做到这一点,但第一种方法真的很... hackish。引用 Ecma-335,IL 的标准文档:

“与 box 不同,它需要复制值类型以在对象中使用,unbox 不需要从对象中复制值类型。通常它只是计算已经存在于内部的值类型的地址装箱的物体。”

因此,如果您想玩危险的游戏,可以使用 OpCodes.Unbox 将您的对象句柄更改为指向您的结构的指针,然后可以将其用作 Stfld 或 Callvirt 的第一个参数。这样做实际上最终会修改结构,您甚至不需要通过 ref 传递目标对象。

但是,请注意,该标准并不能保证 Unbox 会给您一个指向盒装版本的指针。特别是,它表明 Nullable<> 可以导致 Unbox 创建一个副本。无论如何,如果发生这种情况,您可能会遇到静默失败,它会在本地副本上设置字段或属性值,然后立即丢弃。

因此,安全的方法是通过 ref 传递您的对象,将地址存储在局部变量中,进行修改,然后将结果重新装箱并将其放回您的 ByRef 对象参数中。

我做了一些粗略的计时,调用每个版本 10,000,000 次,有 2 种不同的结构:

具有 1 个字段的结构:0.46 秒“不安全”委托 0.70 秒“安全”委托 4.5 秒 FieldInfo.SetValue

具有 4 个字段的结构: .46 秒“不安全”委托 .88 秒“安全”委托 4.5 秒 FieldInfo.SetValue

请注意,装箱使“安全”版本的速度随着结构大小而降低,而其他两种方法不受结构大小的影响。我想在某些时候装箱成本会超过反射成本。但我不会相信任何重要的“不安全”版本。

于 2009-10-05T00:39:11.600 回答
5

经过一些实验:

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);
    }
}

与表达式树方法的主要区别在于只读字段也可以更改。

于 2014-05-16T15:01:46.423 回答
3

此代码适用于不使用 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();
}

这是我的测试代码:

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));
}
于 2015-08-08T20:08:46.807 回答
1

您可能想看看动态方法(反射不必很慢!)...

格哈德对此有一篇很好的文章:http: //jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/

于 2009-08-14T06:59:46.880 回答
0

您可以很容易地修改它以使用结构。它目前是基于字典的,但您的情况更容易。

http://www.damonpayne.com/2009/09/07/TwoWayBindingToNameValuePairs.aspx

于 2010-01-11T01:10:07.637 回答