Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected non-equality after assignment

Tags:

c#

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:

  • The part (a = b) assigns b to a and returns b, so now a equals b.
  • Then, 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?

like image 988
Youssef13 Avatar asked May 29 '20 19:05

Youssef13


People also ask

What happens if we use assignment operator in if condition?

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)

What is == and === in JavaScript?

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.

What happens when we use assignment operator (=) in a selection control?

The assignment operator allows us to change the value of a modifiable data object (for beginning programmers this typically means a variable).

What is === operator in JavaScript?

The strict equality operator ( === ) checks whether its two operands are equal, returning a Boolean result.

What is the unexpected assignment expression error?

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.

What is the unexpected assignment expression warning in JSLint?

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"

What is the strict equality comparison algorithm used for?

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 .

What is the behavior for performing loose equality using ==?

(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.


4 Answers

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.

like image 63
Magnetron Avatar answered Oct 16 '22 20:10

Magnetron


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)
like image 32
Joe Sewell Avatar answered Oct 16 '22 21:10

Joe Sewell


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.

like image 5
Guru Stron Avatar answered Oct 16 '22 20:10

Guru Stron


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 object
  • b variable points to b object
  • a object's x attribute points to b object
  • b 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.

like image 2
Daniel Bider Avatar answered Oct 16 '22 21:10

Daniel Bider