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 ofx
through an implicit conversion.- [...]
- The value resulting from the evaluation and conversion of
y
is stored into the location given by the evaluation ofx
.
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.