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

コードは正常に機能します。さて、リフレクションは遅いので使いたくないとしましょう。代わりに、フィールドを直接変更するCILを生成idし、その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が好きではない理由は、オブジェクトのセッターと構造体のセッターに異なるデリゲートを設定したくないためです(また、set-object-fieldデリゲートを必要以上に複雑にしたくないためです。例Action<object, object>)。の実装でCreateSetIdDelegateは、ターゲットタイプが構造体かオブジェクトかによって異なるCILが生成されると思いますが、同じAPIをユーザーに提供する同じデリゲートを返すようにしたいです。

4

6 に答える 6

14

もう一度編集:これで構造体が機能するようになりました。

C#4でそれを行うためのゴージャスな方法がありますが、ILGeneratorその前に何かのために独自のエミットコードを作成する必要があります。彼らは.NETFramework4にを追加しましたExpressionType.Assign

これは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));
}

そこにある「#ifUNSAFE_IL」のものに注意してください。私は実際にそれを行うための2つの方法を思いついたが、最初の方法は本当に...ハックだ。Ecma-335から引用すると、ILの標準ドキュメントは次のとおりです。

「オブジェクトで使用する値型のコピーを作成するために必要なboxとは異なり、unboxは、オブジェクトから値型をコピーする必要はありません。通常、内部にすでに存在する値型のアドレスを計算するだけです。箱入りのオブジェクト。」

したがって、危険な状態でプレイしたい場合は、OpCodes.Unboxを使用して、オブジェクトハンドルを構造体へのポインターに変更できます。これは、StfldまたはCallvirtの最初のパラメーターとして使用できます。このようにすると、実際には構造体が適切に変更され、ターゲットオブジェクトをrefで渡す必要さえありません。

ただし、この標準では、Unboxがボックス化されたバージョンへのポインタを提供することを保証していないことに注意してください。特に、Nullable<>によってUnboxがコピーを作成する可能性があることを示唆しています。とにかく、それが発生した場合、おそらくサイレントエラーが発生し、ローカルコピーにフィールドまたはプロパティの値が設定され、すぐに破棄されます。

したがって、これを行う安全な方法は、オブジェクトをrefで渡し、アドレスをローカル変数に格納し、変更を加えてから、結果を再ボックス化してByRefオブジェクトパラメーターに戻すことです。

2つの異なる構造で、各バージョンを10,000,000回呼び出すという、大まかなタイミングを実行しました。

1つのフィールドを持つ構造:.46s「安全でない」デリゲート.70s「安全な」デリゲート4.5s FieldInfo.SetValue

4つのフィールドを持つ構造:.46s「安全でない」デリゲート.88s「安全な」デリゲート4.5s FieldInfo.SetValue

ボックス化により、「安全な」バージョンの速度が構造サイズとともに低下するのに対し、他の2つの方法は構造サイズの影響を受けないことに注意してください。ある時点で、ボクシングのコストがリフレクションのコストを上回っていると思います。しかし、私は「安全でない」バージョンを重要な立場で信用しません。

于 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

動的な方法を確認することをお勧めします(反射を遅くする必要はありません!)...

Gerhardはそれについて素晴らしい投稿をしています: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 に答える