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?

Abel
  • 56,041
  • 24
  • 146
  • 247

3 Answers3

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);
    }
}
  • Yes, that's true, but my question is about fields, not about properties. Slight but important difference: fields do not have a dedicated setter. – Abel Mar 29 '12 at 16:31
  • @Abel Suddenly it seems less elegant, but it does still work with fields. (You can cache the created methods if you use them a lot.) –  Mar 29 '12 at 16:45
  • 1
    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. – Abel Mar 29 '12 at 17:09
  • +1 This is an amazing answer! I knew it was possible to get rid of the so annoying `__makeref`, and here it is! – Sergey Kalinichenko Sep 06 '12 at 21:26
  • I tried your `propertySetter` method and I saw a huge performance improvement over using the standard `PropertyInfo.GetSetter.Invoke` method. – Jonathan Allen Nov 24 '17 at 07:04
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);
  }
}
Serj-Tm
  • 16,581
  • 4
  • 54
  • 61
  • 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. – Abel Mar 29 '12 at 16:29
  • 1
    @Abel: CLS Compliant means that your public members only reference CLS-compliant types. It says nothing about what's contained in your members. – Gabe Mar 29 '12 at 16:55
  • Oops, my comment-edit got lost. Yes, I noticed, it's clear from the [top three bullets on MSDN on CLS Compliance](http://msdn.microsoft.com/en-us/library/bhc3fa7f.aspx). Still it feels awfully odd that I need an [undocumented keyword __makeref](http://www.codeproject.com/Articles/38695/UnCommon-C-keywords-A-Look#makref) to get this to work. – Abel Mar 29 '12 at 17:05
  • @Abel 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 also – Serj-Tm Mar 29 '12 at 17:20
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.

Community
  • 1
  • 1
Ian Horwill
  • 2,957
  • 2
  • 24
  • 24
  • 1
    +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. – Abel Mar 29 '12 at 15:08
  • 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? – Ian Horwill Mar 29 '12 at 15:12
  • 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? – Ian Horwill Mar 29 '12 at 15:24
  • 1
    > _"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. – Abel Mar 29 '12 at 16:37
  • Thanks for clearing that up. Was bugging me. Sounds obvious when explained so clearly. – Ian Horwill Mar 31 '12 at 09:51
  • 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. – supercat Apr 02 '12 at 17:37
  • 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. – supercat Apr 02 '12 at 17:43