fabulous.yap

Posted on

Chaining C# Assignment Operators

It all started with `swap()`, a simple programming question added to my interviewing question bank. In its simplest form, a temp variable is introduced as a place holder while we swap two Integer numbers:

``````static void swap(ref int a, ref int b)
{
int tmp;

tmp = a;
a = b;
b = tmp;
}
``````

and of course, we have the following way of writing `swap()` without using a temporary variable:

``````static void swap(ref int a, ref int b)
{
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
``````

which we can further simplify it using Assignment Operator to:

``````static void swap(ref int a, ref int b)
{
a ^= b;
b ^= a;
a ^= b;
}
``````

now, being a nerd coming from `C/C++`, we know we can shorten the above into:

``````/* C using pointer */
void swap(int *a, int *b)
{
*a ^= *b ^= *a ^= *b;
}
``````

in `C` language or `C++`...

``````// C++ using reference
void swap(int& a, int& b)
{
a ^= b ^= a ^= b;
}
``````

using Assignment Operator. These works as Assignment Operators are evaluated in right-associative manner... and here comes the equivalent `C#` codes...

``````static void swap(ref int a, ref int b)
{
a ^= b ^= a ^= b;
}
``````

but I was surprised that the above `C#` codes doesn't work as expected. Try the following program and you'll see it doesn't really swap the 2 integers...

``````using System;

namespace AssignmentOperatorChaining
{
class Program
{
static void Main(string[] args)
{
int x = 0, y = 0;

x = 10; y = 20;
Console.WriteLine("x: {0},    y: {1}", x, y);
swap(ref x, ref y);
Console.WriteLine("x: {0},    y: {1}", x, y);
}

static void swap(ref int a, ref int b)
{
a ^= b ^= a ^= b;
}
}
}
``````

the result is `x: 0, y: 10` instead of `x: 20, y: 10` after `swap()` was called. With some googling, I failed to find much information to this behavior.
Furthermore, a small change to the Assignment chaining code above works:

``````static void swap(ref int a, ref int b)
{
b ^= a ^= b;
a ^= b;
}
``````

At this point, I'm really puzzled... but since .Net disassemblers are widely available, decided to take a peek under the hood to see what's going on. Created the following program to see the differences between a few different way of swapping 2 integers...

``````using System;

namespace AssignmentOperatorChaining
{
class Program
{
static void Main(string[] args)
{
int x = 0, y = 0;

x = 10; y = 20;
Console.WriteLine("x: {0},    y: {1}", x, y);
swap1(ref x, ref y);
Console.WriteLine("x: {0},    y: {1}", x, y);

x = 10; y = 20;
Console.WriteLine("x: {0},    y: {1}", x, y);
swap2(ref x, ref y);
Console.WriteLine("x: {0},    y: {1}", x, y);

x = 10; y = 20;
Console.WriteLine("x: {0},    y: {1}", x, y);
swap3(ref x, ref y);
Console.WriteLine("x: {0},    y: {1}", x, y);

x = 10; y = 20;
Console.WriteLine("x: {0},    y: {1}", x, y);
swap4(ref x, ref y);
Console.WriteLine("x: {0},    y: {1}", x, y);

x = 10; y = 20;
Console.WriteLine("x: {0},    y: {1}", x, y);
swap5(ref x, ref y);
Console.WriteLine("x: {0},    y: {1}", x, y);
}

static void swap1(ref int a, ref int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
static void swap2(ref int a, ref int b)
{
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
static void swap3(ref int a, ref int b)
{
a ^= b;
b ^= a;
a ^= b;
}
static void swap4(ref int a, ref int b)
{
b ^= a ^= b;
a ^= b;
}
static void swap5(ref int a, ref int b)
{
a ^= b ^= a ^= b;
}
}
}
``````

and the resulting il...

very interesting output. and what's more interesting is when feeding the exe to .Net disassembler like JustDecompile, the reverse engineer `C#` code for `swap5()` actually works:

``````private static void swap(ref int a, ref int b)
{
int num = a ^ b;
int num1 = num;
a = num;
int num2 = b ^ num1;
num1 = num2;
b = num2;
a ^= num1;
}
``````

(I've also tested with JetBrains' dotPeek, however, the disassembled output of `swap5()`triggers a panic mode in dotPeek and resulted in uncompile `C#` codes.)

if we take a closer look, there is really no difference between `swap2()` and `swap3()`, and it appears `swap1()` resulted in the shortest `IL` code and this can also be confirm when running in disassembling x86 mode:

regardless of whether we consider the `a ^= b ^= a ^= b` Assignment Operator Chaining is a `C#` compiler bug (tests has also been performed using .Net Core 2.1 in both Windows and Linux, same results), the moral of the story here is sometimes, the simplest thing could often be the best and/or fastest -- K.I.S.S. not to mention at the same time swap1() is more human readable too.

this Assignment Operator Chaining strange behavior not only affects `^=` operator, others are affected too, here is a small program to test:

``````using System;

namespace AssignmentOperatorsChaining
{
class Program
{
static void Main(string[] args)
{
int x=0, y=0;

Console.WriteLine("-----------========== ^= operator ===========-----------");
Console.WriteLine("chaining:");
x = 10; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x ^= y ^= x ^= y;
Console.WriteLine("x: {0},    y:{1}", x, y);

Console.WriteLine("non-chaining:");
x = 10; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x ^= y;
y ^= x;
x ^= y;
Console.WriteLine("x: {0},    y:{1}", x, y);
Console.WriteLine("");

Console.WriteLine("-----------========== *= operator ===========-----------");
Console.WriteLine("chaining:");
x = 10; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x *= y *= x *= y;
Console.WriteLine("x: {0},    y:{1}", x, y);

Console.WriteLine("non-chaining:");
x = 10; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x *= y;
y *= x;
x *= y;
Console.WriteLine("x: {0},    y:{1}", x, y);
Console.WriteLine("");

Console.WriteLine("-----------========== += operator ===========-----------");
Console.WriteLine("chaining:");
x = 10; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x += y += x += y;
Console.WriteLine("x: {0},    y:{1}", x, y);

Console.WriteLine("non-chaining:");
x = 10; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x += y;
y += x;
x += y;
Console.WriteLine("x: {0},    y:{1}", x, y);
Console.WriteLine("");

Console.WriteLine("-----------========== -= operator ===========-----------");
Console.WriteLine("chaining:");
x = 10; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x -= y -= x -= y;
Console.WriteLine("x: {0},    y:{1}", x, y);

Console.WriteLine("non-chaining:");
x = 10; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x -= y;
y -= x;
x -= y;
Console.WriteLine("x: {0},    y:{1}", x, y);
Console.WriteLine("");

Console.WriteLine("-----------========== /= operator ===========-----------");
Console.WriteLine("chaining:");
x = 20; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x /= y /= x /= y;
Console.WriteLine("x: {0},    y:{1}", x, y);

Console.WriteLine("non-chaining:");
x = 20; y = 20;
Console.WriteLine("x: {0},    y:{1}", x, y);
x /= y;
y /= x;
x /= y;
Console.WriteLine("x: {0},    y:{1}", x, y);