Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does C# evaluate expressions which contain assignments?

Tags:

c#

I had C/C++ background. I came across a strange way of exchanging two values in C#.

int n1 = 10, n2=20;
n2 = n1 + (n1=n2)*0;

In C#, the above two lines do swap values between n1 and n2. This is a surprise to me as in C/C++, the result should be n1=n2=20.

So, how does C# evaluate an expression? It looks like the + above is treated as a function calling to me. The following explainion seems reasoable. BUT seems way weired to me.

  1. First (n1=n2) is executed. And thus n1=20.
  2. Then n1 in n1+ (n1=n2)*0 is not 20 yet. It is treated as a function parameter, thus pushed on the stack and is still 10. Therefore, n2=10+0=10.
like image 538
Peng Zhang Avatar asked Jul 30 '14 05:07

Peng Zhang


3 Answers

In C#, sub-expressions are evaluated in left-to-right order, with side-effects produced in that order. This is defined in section 7.3 of the C# 5 specification:

Operands in an expression are evaluated from left to right.

It important to realize that the order of sub-expression evaluation is independent of precedence (aka order of operations) and associativity. For example, in an expression like A() + B() * C(). The evaluation order in C# is always A(), B(), C(). My limited understanding of C/C++ is that this order is a compiler implementation detail.

In your example, first n1 (10) is evaluated for the left operand of +. Then (n1=n2) is evaluated. The result of this is value of n2 (20), and the side-effect of assigning to n1 is produced. n1 is now 20. Then multiplication of 20 * 0 occurs producing 0. Then 10 + 0 is computed and the result (10) is assigned to n2. Therefore, the expected state at the end is that n1 = 20 and n2 = 10.

Eric Lippert has discussed this issue at length on this site and his blog.

like image 188
Mike Zboray Avatar answered Oct 27 '22 11:10

Mike Zboray


Ok so this is probably best to explain using IL opcodes.

IL_0000:  ldc.i4.s    0A 
IL_0002:  stloc.0     // n1
IL_0003:  ldc.i4.s    14 
IL_0005:  stloc.1     // n2

The first 4 lines are kinda self explanatory ldc.i4 loads the variable (int of size 4) only the stack while stloc.* store the value at the top of the stack

IL_0006:  ldloc.0     // n1
IL_0007:  ldloc.1     // n2
IL_0008:  stloc.0     // n1
IL_0009:  stloc.1     // n2

These lines are essentially what you have described. Each values is loaded only the stack, n1 before n2 and then stored but with n1 being stored before n2 ( therefore swapping )

This I believe is the correct behaviour as described in the .NET specification.

mikez also added more detail and help me track down the answer but i believe the answer is really explained in 7.3.1

When an operand occurs between two operators with the same precedence, the associativity of the operators controls the order in which the operations are performed:

  • Except for the assignment operators and the null coalescing operator, all binary operators are left-associative, meaning that operations are performed from left to right. For example, x + y + z is evaluated as (x + y) + z.

  • The assignment operators, the null coalescing operator and the conditional operator (?:) are right-associative, meaning that operations are performed from right to left. For example, x = y = z is evaluated as x = (y = z). Precedence and associativity can be controlled using parentheses. For example, x + y * z first multiplies y by z and then adds the result to x, but (x + y) * z first adds x and y and then multiplies the result by z.

What is important here is the order at which the operations are evaluated so what is actually being evaluated is

n2 = (n1) + ((n1=n2)*0)

where (n1) + (..) is evaluated left to right by being a binary operator.

like image 34
Alistair Avatar answered Oct 27 '22 12:10

Alistair


Read the spec, it will tell you the truth:

7.5.1.2 Run-time evaluation of argument lists

The expressions of an argument list are always evaluated in the order they are written. Thus, the example

class Test
{
  static void F(int x, int y = -1, int z = -2) {
      System.Console.WriteLine("x = {0}, y = {1}, z = {2}", x, y, z);
  }
  static void Main() {
      int i = 0;
      F(i++, i++, i++);
      F(z: i++, x: i++);
  }
}

produces the output

x = 0, y = 1, z = 2
x = 4, y = -1, z = 3

You can see that it applies to arithmetical operations too, if you change your code to:

int n1 = 10, n2=20;
n2 = (n1=n2) * 0 + n1;

Now, both n1 and n2 equal 20.

like image 36
MarcinJuraszek Avatar answered Oct 27 '22 11:10

MarcinJuraszek