Given the following code:
using System;
class MyClass
{
public MyClass x;
}
public static class Program
{
public static void Main()
{
var a = new MyClass();
var b = new MyClass();
a.x = (a = b);
Console.WriteLine(a.x == a);
}
}
The first two lines are very obvious, just two different objects.
I assume the third line to do the following:
(a = b)
assigns b
to a
and returns b
, so now a
equals b
.a.x
is assigned to b
.That means, a.x
equals to b
, and also b
equals to a
. Which implies that a.x
equals to a
.
However, the code prints False
.
What's going on?
Using the assignment operator in conditional expressions frequently indicates programmer error and can result in unexpected behavior. The assignment operator should not be used in the following contexts: if (controlling expression) while (controlling expression)
The main difference between the == and === operator in javascript is that the == operator does the type conversion of the operands before comparison, whereas the === operator compares the values as well as the data types of the operands.
The assignment operator allows us to change the value of a modifiable data object (for beginning programmers this typically means a variable).
The strict equality operator ( === ) checks whether its two operands are equal, returning a Boolean result.
The "Unexpected assignment expression" error (and the alternative "Expected a conditional expression and instead saw an assignment" error) are thrown when JSLint, JSHint or ESLint encounters an assignment expression in an if, for or while statement initializer.
In JSLint, up until July 2013, the warning given was "Expected a conditional expression and instead saw an assignment" In July 2013 the warning given by JSLint changed to "Unexpected assignment expression" In both JSHint and ESLint the warning has always been "Expected a conditional expression and instead saw an assignment"
The strict equality operators ( === and !==) use the Strict Equality Comparison Algorithm to compare two operands. If the operands are of different types, return false. If both operands are objects, return true only if they refer to the same object. If both operands are null or both operands are undefined , return true .
(The only case in which (x !== x) is true is when x is NaN .) The behavior for performing loose equality using == is as follows: Loose equality compares two values for equality after converting both values to a common type.
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.
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.
Interesting find, I've put your code into Sharplab and checked what happens.
Seems like compiler swaps left operands in your assignment, this is what it looks like decompiled back to C# (variable names are changed):
public static void Main()
{
MyClass myClass = new MyClass();
MyClass x = new MyClass();
myClass = (myClass.x = x);
Console.WriteLine(myClass.x == myClass);
}
So what happens is a.x
becomes b
and then b
is assigned to a
. Both local variable a
and attribute a.x
now point to b
object.
So:
a
variable points to b
objectb
variable points to b
objecta
object's x
attribute points to b
objectb
object's x
attribute is null I changed your code a bit to illustrate that better:
public static void Main(string[] args)
{
var a = new MyClass();
var originalA = a;
a.Name = "a";
var b = new MyClass();
b.Name = "b";
a.x = (a = b);
Console.WriteLine(a.x == a);
Console.WriteLine("a - " + a.Name);
Console.WriteLine("a.x - " + a.x?.Name);
Console.WriteLine("b - " + b.Name);
Console.WriteLine("b.x - " + b.x?.Name);
Console.WriteLine("originalA - " + originalA.Name);
Console.WriteLine("originalA.x - " + originalA.x?.Name);
}
That code returns:
False
a - b
a.x -
b - b
b.x -
originalA - a
originalA.x - b
Notice only originalA
now points to actual a
object, other local variables now point to b
.
It's not a compiler bug - see Magnetron's answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With