Can I set a value on a struct through reflection without boxing?
Asked Answered
B

3

11

Actually, I should've asked: how can I do this and remain CLS Compliant? Because the only way I can think of doing this is as follows, but using either __makeref, FieldInfo.SetValueDirect or just System.TypedReference in general invalidates CLS Compliance.

// code illustrating the issue:
TestFields fields = new TestFields { MaxValue = 1234 };  // test struct with one field

FieldInfo info = fields.GetType().GetField("MaxValue");  // get the FieldInfo

// actual magic, no boxing, not CLS compliant:
TypedReference reference = __makeref(fields);
info.SetValueDirect(reference, 4096);

The compliant counterpart of SetValueDirect is SetValue, but it takes an object as the target, hence my struct will be boxed, making me setting a value on a copy, not the original variable.

A generic counterpart for SetValue doesn't exist as far as I know. Is there any other way of setting the field of a (reference to a) struct through reflection?

Batey answered 29/3, 2012 at 14:36 Comment(0)
M
7

Make cls-compliant wrapper on SetValueDirect:

  var item = new MyStruct { X = 10 };

  item.GetType().GetField("X").SetValueForValueType(ref item, 4);


[CLSCompliant(true)]
static class Hlp
{
  public static void SetValueForValueType<T>(this FieldInfo field, ref T item, object value) where T : struct
  {
    field.SetValueDirect(__makeref(item), value);
  }
}
Macrography answered 29/3, 2012 at 15:15 Comment(4)
Maybe I don't understand CLSCompliancy, I thought it meant you could not use non-compliant features. If this is allowed, it makes things a whole lot easier indeed.Batey
@Abel: CLS Compliant means that your public members only reference CLS-compliant types. It says nothing about what's contained in your members.Christal
Oops, my comment-edit got lost. Yes, I noticed, it's clear from the top three bullets on MSDN on CLS Compliance. Still it feels awfully odd that I need an undocumented keyword __makeref to get this to work.Batey
@Batey Maybe you mix up clscompliant/non-clscompliant with safe/non-safe. For code is cls-compliant then public member signatures must be cls-compliant only, but for code is safe then body of methods must be safe alsoMacrography
B
8

For properties, if you have the struct and property types, you can create a delegate from the property setter. As you point out, fields don't have setters, but you can create one that behaves exactly the same:

delegate void RefAction<T1, T2>(ref T1 arg1, T2 arg2);

struct TestFields
{
    public int MaxValue;

    public int MaxValueProperty
    {
        get { return MaxValue; }
        set { MaxValue = value; }
    }
};

static class Program
{
    static void Main(string[] args)
    {
        var propertyInfo = typeof(TestFields).GetProperty("MaxValueProperty");
        var propertySetter = (RefAction<TestFields, int>)Delegate.CreateDelegate(typeof(RefAction<TestFields, int>), propertyInfo.GetSetMethod());

        var fieldInfo = typeof(TestFields).GetField("MaxValue");

        var dynamicMethod = new DynamicMethod(String.Empty, typeof(void), new Type[] { fieldInfo.ReflectedType.MakeByRefType(), fieldInfo.FieldType }, true);
        var ilGenerator = dynamicMethod.GetILGenerator();
        ilGenerator.Emit(OpCodes.Ldarg_0);
        ilGenerator.Emit(OpCodes.Ldarg_1);
        ilGenerator.Emit(OpCodes.Stfld, fieldInfo);
        ilGenerator.Emit(OpCodes.Ret);
        var fieldSetter = (RefAction<TestFields, int>)dynamicMethod.CreateDelegate(typeof(RefAction<TestFields, int>));

        var fields = new TestFields { MaxValue = 1234 };
        propertySetter(ref fields, 5678);
        fieldSetter(ref fields, 90);
        Console.WriteLine(fields.MaxValue);
    }
}
Bond answered 29/3, 2012 at 16:25 Comment(5)
Yes, that's true, but my question is about fields, not about properties. Slight but important difference: fields do not have a dedicated setter.Batey
@Batey Suddenly it seems less elegant, but it does still work with fields. (You can cache the created methods if you use them a lot.)Bond
So now we have (1) __makeref undocumented keyword, (2) emit IL opcodes. Not sure which of the two I prefer, they both seem so overly convoluted. But Emit is CLS Compliant and TypedReference is not.Batey
+1 This is an amazing answer! I knew it was possible to get rid of the so annoying __makeref, and here it is!Mocha
I tried your propertySetter method and I saw a huge performance improvement over using the standard PropertyInfo.GetSetter.Invoke method.Zelikow
M
7

Make cls-compliant wrapper on SetValueDirect:

  var item = new MyStruct { X = 10 };

  item.GetType().GetField("X").SetValueForValueType(ref item, 4);


[CLSCompliant(true)]
static class Hlp
{
  public static void SetValueForValueType<T>(this FieldInfo field, ref T item, object value) where T : struct
  {
    field.SetValueDirect(__makeref(item), value);
  }
}
Macrography answered 29/3, 2012 at 15:15 Comment(4)
Maybe I don't understand CLSCompliancy, I thought it meant you could not use non-compliant features. If this is allowed, it makes things a whole lot easier indeed.Batey
@Abel: CLS Compliant means that your public members only reference CLS-compliant types. It says nothing about what's contained in your members.Christal
Oops, my comment-edit got lost. Yes, I noticed, it's clear from the top three bullets on MSDN on CLS Compliance. Still it feels awfully odd that I need an undocumented keyword __makeref to get this to work.Batey
@Batey Maybe you mix up clscompliant/non-clscompliant with safe/non-safe. For code is cls-compliant then public member signatures must be cls-compliant only, but for code is safe then body of methods must be safe alsoMacrography
F
2

Not sure if this will fit into your constraints, but by declaring the struct instance as ValueType, SetValue will work as expected.

    ValueType fields = new TestFields { MaxValue = 1234 };  // test struct with one field
    FieldInfo info = typeof(TestFields).GetField("MaxValue");  // get the FieldInfo
    info.SetValue(fields, 4096);
    Console.WriteLine(((TestFields)fields).MaxValue);  // 4096

See this answer for some more info.

Fetching answered 29/3, 2012 at 14:56 Comment(7)
+1: Your answer is certainly interesting, but your code does not operate on the original struct. Instead, it is boxed for SetValue and unboxed because you cast it for use with WriteLine. To see that for yourself, check the IL.Batey
I didn't look at the IL but I verified that the last line does indeed display 4096, so how can it not be operating on the original instance?Fetching
OK, now I did look at the IL. I'm not very fluent but I see the box calls you're talking about. From the locals section, it is declaring two instances of TestFields when the C# only has one. Very strange. Is it the actual boxing you care about or that the value gets set correctly?Fetching
> "how can it not be operating on the original instance": the minute you pass a value type to a method that accepts type object, or when you call an instance method (i.e. 1.ToString()), the valye type is boxed. You can force boxing with object o = anyvaluetype, which can be used instead of your first line. Because (odd, I know) ValueType is not a value type but a class, you forced boxing in that first line. Result, SetValue does not do additional boxing, instead it operates on the object fields. Casting this object back to a struct unboxes it.Batey
Thanks for clearing that up. Was bugging me. Sounds obvious when explained so clearly.Fetching
Some people like to pretend that value types derive from System.ValueType, which in turn derives from System.Object; that's only half true. An instance of a value-type object will indeed match that description, but value-type storage locations do not hold such instances. Instead, they simply hold the contents of all public and private fields that such objects would contain. A widening conversion exists from each value-type-storage-location type to its corresponding object-instance type, and narrowing reverse conversions from System.Object, interface types, or class-constrained generics.Salve
Note that while the C# language spec states that the thing held by a value-type storage location is the same type as a value-type object instance, and while it is true that are both described by the same System.Type object, they actually behave as though they are different types. For example, a cast from the latter to Object is reference-preserving, while a cast from the former to Object is not. Note that one can't actually call GetType on a value-type storage location. Instead, a new object instance of its type will be created, and GetType called on that.Salve

© 2022 - 2024 — McMap. All rights reserved.