C# Reflection - How to set field value for struct

2019-03-25 16:50发布

问题:

How can I set value into struct field - myStruct.myField with reflection using DynamicMethod? When I call setter(myStruct, 111) value was not set, because MyStruct is value type. Console.WriteLine(myStruct.myField) shows value 3.
How to modify GetDelegate method to set value into myStruct.myField?

public struct MyStruct
{
    public int myField;
}

public delegate void SetHandler(object source, object value);

private static SetHandler GetDelegate(Type type, FieldInfo fieldInfo)
{
    DynamicMethod dm = new DynamicMethod("setter", typeof(void), new Type[] { typeof(object), typeof(object) }, type, true);
    ILGenerator setGenerator = dm.GetILGenerator();

    setGenerator.Emit(OpCodes.Ldarg_0);
    setGenerator.DeclareLocal(type);
    setGenerator.Emit(OpCodes.Unbox_Any, type);
    setGenerator.Emit(OpCodes.Stloc_0);
    setGenerator.Emit(OpCodes.Ldloca_S, 0);
    setGenerator.Emit(OpCodes.Ldarg_1);
    setGenerator.Emit(OpCodes.Unbox_Any, fieldInfo.FieldType);
    setGenerator.Emit(OpCodes.Stfld, fieldInfo);
    setGenerator.Emit(OpCodes.Ldloc, 0);
    setGenerator.Emit(OpCodes.Box, type);
    setGenerator.Emit(OpCodes.Ret);
    return (SetHandler)dm.CreateDelegate(typeof(SetHandler));
}

MyStruct myStruct = new MyStruct();
myStruct.myField = 3;

FieldInfo fi = typeof(MyStruct).GetField("myField", BindingFlags.Public | BindingFlags.Instance);

SetHandler setter = GetDelegate(typeof(MyStruct), fi);
setter(myStruct, 111);
Console.WriteLine(myStruct.myField);

回答1:

Edit: I made this mistake again - fun fact; unbox-any returns the value; unbox returns the pointer to the data - which allows in-place mutate.

Here's the working IL generation:

    setGenerator.Emit(OpCodes.Ldarg_0);
    setGenerator.Emit(OpCodes.Unbox, type);
    setGenerator.Emit(OpCodes.Ldarg_1);
    setGenerator.Emit(OpCodes.Unbox_Any, fieldInfo.FieldType);
    setGenerator.Emit(OpCodes.Stfld, fieldInfo);
    setGenerator.Emit(OpCodes.Ret);

But! This is mutating a boxed copy; you would need to unbox afterwards:

    object obj = myStruct;
    setter(obj, 111);
    MyStruct andBackAgain = (MyStruct)obj;
    Console.WriteLine(andBackAgain.myField);
    Console.WriteLine(myStruct.myField);

To do it in place, you would probably need the method to take a ref MyStruct, or to return a MyStruct. You could return the boxed copy, but that doesn't make it much easier to use. Frankly, it is moot: structs should not generally be mutable.



回答2:

I think as Servy points out in the comments it's probably better to not have a mutable struct and instead create a copy of the struct with the new value. You can use the SetValueDirect method on the FieldInfo instance if you really want to use reflection to mutate the struct but it uses the undocumented __makeref method to get a TypedReference.

I really wouldn't recommend using this code; it's purely to show that this is possible:

MyStruct myStruct = new MyStruct();
myStruct.myField = 3;

FieldInfo fi = typeof(MyStruct).GetField("myField", BindingFlags.Public | BindingFlags.Instance);
TypedReference reference = __makeref(myStruct);
fi.SetValueDirect(reference, 111);
Console.WriteLine(myStruct.myField); //prints 111


回答3:

Here the code with ref:

public delegate void SetHandler<T>(ref T source, object value) where T : struct;

        private static SetHandler<T> GetDelegate<T>(FieldInfo fieldInfo) where T : struct
        {
            var type = typeof(T);
            DynamicMethod dm = new DynamicMethod("setter", typeof(void), new Type[] { type.MakeByRefType(), typeof(object) }, type, true);
            ILGenerator setGenerator = dm.GetILGenerator();

            setGenerator.Emit(OpCodes.Ldarg_0);
            setGenerator.DeclareLocal(type);
            setGenerator.Emit(OpCodes.Ldarg_0);
            setGenerator.Emit(OpCodes.Ldnull);
            setGenerator.Emit(OpCodes.Stind_Ref);
            setGenerator.Emit(OpCodes.Ldarg_1);
            setGenerator.Emit(OpCodes.Unbox_Any, fieldInfo.FieldType);
            setGenerator.Emit(OpCodes.Stfld, fieldInfo);
            setGenerator.Emit(OpCodes.Ldloc, 0);
            setGenerator.Emit(OpCodes.Box, type);
            setGenerator.Emit(OpCodes.Ret);
                return (SetHandler<T>)dm.CreateDelegate(typeof(SetHandler<>).MakeGenericType(type));
        }

        static void Main(string[] args)
        {
            MyStruct myStruct = new MyStruct();
            myStruct.myField = 3;

            FieldInfo fi = typeof(MyStruct).GetField("myField", BindingFlags.Public | BindingFlags.Instance);

            var setter = GetDelegate<MyStruct>(fi);
            setter(ref myStruct, 111);
            Console.WriteLine(myStruct.myField);
        }


回答4:

The easiest way to use Reflection to set fields or properties of a structure is to box the structure, pass the boxed structure to SetField or SetProperty, and then unbox the structure once all desired manipulations are complete.

public static class refStructTest
{
    struct S1
    {
        public int x;
        public int y { get; set; }
        public override string ToString()
        {
            return String.Format("[{0},{1}]", x, y);
        }
    }
    public static void test()
    {
        var s = default(S1);
        s.x = 2;
        s.y = 3;
        Object obj = s;
        var fld = typeof(S1).GetField("x");
        var prop = typeof(S1).GetProperty("y");
        fld.SetValue(obj,5);
        prop.SetValue(obj,6,null);
        s = (S1)obj;
        Console.WriteLine("Result={0}", s);
    }
}

According to the ECMA documentation, each value type is associated with two kinds of things: a storage location type and a heap object type. The heap object type, like all heap object types, will behave with reference semantics; passing a reference to a heap object to a method like SetValue will thus modify the object to which the reference was passed.

Note for VB users: VB.NET has a really annoying behavior which almost make sense in the Option Strict On dialect, but which exists even in the Option Strict Off dialect: if a variable of compile-time type Object which holds a reference to a boxed structure is assigned to another variable of that same type or passed as a parameter of type Object, VB.NET will store or pass a reference to a copy of the original object. When writing code like the above in VB.NET, one should make obj be of type ValueType rather than Object to prevent such behavior.



回答5:

To add to other answers, you can actually box inside the delegate, as long as your method also returns the modified struct.

Since my IL-fu is not that great, this is how you would do it with plain reflection:

// the good side is that you can use generic delegates for added type safety
public delegate T SetHandler<T>(T source, object value);

private static SetHandler<T> GetDelegate<T>(FieldInfo fieldInfo)
{
    return (s, val) => 
    { 
        object obj = s; // we have to box before calling SetValue
        fieldInfo.SetValue(obj, val);
        return (T)obj; 
    };
}

This means you will need to fetch the return value like this:

SetHandler<MyStruct> setter = GetDelegate<MyStruct>(fi);
myStruct = setter(myStruct, 111);
Console.WriteLine(myStruct.myField);

But there's no need to box it before calling the setter.

Alternatively, you can pass the struct using the ref keyword, which would result in:

public delegate void SetHandler<T>(ref T source, object value);

private static SetHandler<T> GetDelegate<T>(FieldInfo fieldInfo)
{
    return (ref T s, object val) => 
    { 
        object obj = s;
        fieldInfo.SetValue(obj, val);
        s = (T)obj; 
    };
}

SetHandler<MyStruct> setter = GetDelegate<MyStruct>(fi);
setter(ref myStruct, 111); // no need to return anymore
Console.WriteLine(myStruct.myField);