5

I want to set private fields using LINQ expressions. I have this code:

//parameter "target", the object on which to set the field `field`
ParameterExpression targetExp = Expression.Parameter(typeof(object), "target");

//parameter "value" the value to be set in the `field` on "target"
ParameterExpression valueExp = Expression.Parameter(typeof(object), "value");

//cast the target from object to its correct type
Expression castTartgetExp = Expression.Convert(targetExp, type);

//cast the value to its correct type
Expression castValueExp = Expression.Convert(valueExp, field.FieldType);

//the field `field` on "target"
MemberExpression fieldExp = Expression.Field(castTartgetExp, field);

//assign the "value" to the `field` 
BinaryExpression assignExp = Expression.Assign(fieldExp, castValueExp);

//compile the whole thing
var setter = Expression.Lambda<Action<object, object>> (assignExp, targetExp, valueExp).Compile();

This compiles a delegate that takes two objects, the target and the value:

setter(someObject, someValue);

The type variable specifies the Type of the target, and the field variable is a FieldInfo that specifies the field to be set.

This works great for reference types, but if the target is a struct, then this thing will pass the target as a copy to the setter delegate and set the value on the copy, instead of setting the value on the original target like I want. (At least that is what I think is going on.)

On the other hand,

field.SetValue(someObject, someValue);

works just fine, even for structs.

Is there anything I can do about this in order to set the field of the target using the compiled expression?

Michael Liu
  • 52,147
  • 13
  • 117
  • 150
Roger Johansson
  • 22,764
  • 18
  • 97
  • 193

1 Answers1

6

For value types, use Expression.Unbox instead of Expression.Convert.

//cast the target from object to its correct type
Expression castTartgetExp = type.IsValueType
    ? Expression.Unbox(targetExp, type)
    : Expression.Convert(targetExp, type);

Here's a demo: .NET Fiddle


Q: The setter method doesn't have a ref parameter. How can it update the original struct?

A: Although it's true that, without the ref keyword, value types are normally passed by value and thus copied, here the type of the target parameter is object. If the argument is a boxed struct, then a reference to the box is passed (by value) to the method.

Now, it's not possible using pure C# to mutate a boxed struct because a C# unboxing conversion always produces a copy of the boxed value. But it is possible using IL or Reflection:

public struct S { public int I; }

public void M(object o, int i)
{
    // ((S)o).I = i; // DOESN'T COMPILE
    typeof(S).GetField("I").SetValue(o, i);
}

public void N()
{
    S s = new S();
    object o = s; // create a boxed copy of s

    M(o, 1); // mutate o (but not s)
    Console.WriteLine(((S)o).I); // "1"
    Console.WriteLine(s.I);      // "0"

    M(s, 2); // mutate a TEMPORARY boxed copy of s (BEWARE!)
    Console.WriteLine(s.I);      // "0"
}

Q: Why doesn't the setter work if the LINQ expression uses Expression.Convert?

A: Expression.Convert compiles to the unbox.any IL instruction, which returns a copy of the struct referenced by target. The setter then updates this copy (which is subsequently discarded).

Q: Why does Expression.Unbox fix the problem?

A: Expression.Unbox (when used as the target of Expression.Assign) compiles to the unbox IL instruction, which returns a pointer to the struct referenced by target. The setter then uses the pointer to modify that struct directly.

Michael Liu
  • 52,147
  • 13
  • 117
  • 150
  • This the problem. Not the answer. – leppie Aug 22 '15 at 17:16
  • You are ignoring the fact that the struct is passed into a lambda (and even untyped, it is not going to make a difference). – leppie Aug 22 '15 at 17:57
  • still, Im as curious as @leppie on why the valuetype is not copied when passing it into the delegate (?) – Roger Johansson Aug 22 '15 at 18:21
  • @leppie: I've updated my answer with a (hopefully) clear explanation. – Michael Liu Aug 23 '15 at 20:47
  • Thank you very much for this solution. I cannot wrap my head round one thing, though: In the demo, if I declare the variable as `object`, it works, but as `S`, it will fail. Could you please direct me towards an explanation for that? – tethered.sun Apr 24 '23 at 14:41
  • 1
    @tethered.sun: If the variable `o` is declared as `S` rather than `object`, then `M(o, 1)` is equivalent to `M((object)o, 1)`, where `(object)o` represents a [boxing conversion](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing). The result of `(object)o` is a *temporary boxed copy* of `o`. This copy is mutated by `M` and then discarded, leaving the original `o` unchanged. In contrast, if `o` is declared as `object`, then `M(o, 1)` does not involve any boxing conversion or copying. – Michael Liu Apr 24 '23 at 15:09
  • @MichaelLiu Thank you kindly. I am trying to implement my own custom binary serialisation and your solution is essential in speeding up reflexion-based member access. – tethered.sun Apr 25 '23 at 08:22