Using FieldInfo.SetValue vs LINQ expressions to set a field in a struct
Asked Answered
L

1

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?

Leucippus answered 22/8, 2015 at 16:35 Comment(1)
As you cant use ref, the only way would be assign the return value.Rockhampton
B
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.

Batton answered 22/8, 2015 at 17:12 Comment(7)
This the problem. Not the answer.Rockhampton
You are ignoring the fact that the struct is passed into a lambda (and even untyped, it is not going to make a difference).Rockhampton
still, Im as curious as @Rockhampton on why the valuetype is not copied when passing it into the delegate (?)Leucippus
@leppie: I've updated my answer with a (hopefully) clear explanation.Batton
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?Ilium
@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. 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.Batton
@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.Ilium

© 2022 - 2024 — McMap. All rights reserved.