DEV Community

fabulous.yap
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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

in C language or C++...

// C++ using reference
void swap(int& a, int& b)
{
  a ^= b ^= a ^= b;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
            Console.ReadLine();
        }

        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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

and the resulting il...
IL codes Compare
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;
}
Enter fullscreen mode Exit fullscreen mode

(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:
x86 instructions

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);
            Console.ReadKey();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Last but not least, if this has been documented/discussed somewhere before, or I've made mistakes in the codes, would greatly appreciate the collective wisdom of dev.to community to point me to the right direction. Thank you and good day to all.

Top comments (1)

Collapse
 
bernd5 profile image
Bernd Baumanns

Here you can find a good thread about it: stackoverflow.com/questions/546747...