Unexpected non-equality after assignment

In a.x = (a = b), the left hand side a.x is evaluated first to find the assignment target, then the right hand side is evaluated.

This was also surprising to me, because I intuitively think it starts on the rightmost side and evaluates leftward, but this is not the case. (The associativity is right-to-left, meaning the parentheses in this case are not needed.)

Here's the specification calling out the order things happen in, with the relevant bits quoted below:

The run-time processing of a simple assignment of the form x = y consists of the following steps:

  • If x is classified as a variable:
    • x is evaluated to produce the variable.
    • y is evaluated and, if required, converted to the type of x through an implicit conversion.
    • [...]
    • The value resulting from the evaluation and conversion of y is stored into the location given by the evaluation of x.

Looking at the IL generated by the sharplab link Pavel posted:

        // stack is empty []
newobj instance void MyClass::.ctor()
        // new instance of MyClass on the heap, call it $0
        // stack -> [ref($0)]
stloc.0
        // stack -> []
        // local[0] ("a") = ref($0)
newobj instance void MyClass::.ctor()
        // new instance of MyClass on the heap, call it $1
        // stack -> [ref($1)]
stloc.1
        // stack -> []
        // local[1] ("b") = ref($1)
ldloc.0
        // stack -> [ref($0)]
ldloc.1
        // stack -> [ref($1), ref($0)]
dup
        // stack -> [ref($1), ref($1), ref($0)]
stloc.0
        // stack -> [ref($1), ref($0)]
        // local[0] ("a") = ref($1)
stfld class MyClass MyClass::x
        // stack -> []
        // $0.x = ref($1)

Just to add some IL fun into the discussion:

The Main method header looks next way:

method private hidebysig static void
    Main() cil managed
  {
    .maxstack 3
    .locals init (
      [0] class MyClass a,
      [1] class MyClass b
    )

The a.x = (a=b); statement is translated to the next IL:

IL_000d: ldloc.0      // a
IL_000e: ldloc.1      // b
IL_000f: dup
IL_0010: stloc.0      // a
IL_0011: stfld        class MyClass::x

First two instructions load (ldloc.0, ldloc.1) onto evaluation stack references stored in a and b variables, lets call them aRef and bRef, so we have next evaluation stack state:

bRef
aRef

The dup instruction copies the current topmost value on the evaluation stack, and then pushes the copy onto the evaluation stack:

bRef
bRef
aRef

The stloc.0 pops the current value from the top of the evaluation stack and stores it in a the local variable list at index 0 (a variable is set to bRef), leaving stack in next state:

bRef
aRef

And finally stfld poppes from the stack the value (bRef) and the object reference/pointer (aRef). The value of field in the object (aRef.x) is replaced with the supplied value (bRef).

Which all result in the behavior described in the post, with both variables (a and b) pointing to the bRef with bRef.x being null and aRef.x pointing to bRef, which can be checked with extra variable containing aRef as @Magnetron suggested.


It happens because you're trying to update a twice in the same statement. a in a.x= refers to the old instance. So, you're updating a to reference b and the old a object field x to reference b.

You can confirm with this:

void Main()
{
    var a = new MyClass(){s="a"};
    var b = new MyClass() {s="b"};
    var c =a;

    a.x = (a=b);
    Console.WriteLine($"a is {a.s}");
    Console.WriteLine(a.x == b);

    Console.WriteLine($"c is {c.s}");       
    Console.WriteLine(c.x == b);
}

class MyClass
{
    public MyClass x;
    public string s;
}

The answer will be:

a is b
False
c is a
True

Edit: Just to make a little bit more clear, It's not about the operators' execution order, it's because of the two updates in the same variable in the same statement. The assigment (a=b) is executed before the a.x=, but it doesn't matter, because a.x is referencing the old instance, not the newly updated one. This happens, as @Joe Sewell answer explains, because evaluation, to find the assignment target, is left to right.

Tags:

C#