C Sharp

Compound Assignment Operators

A compound assignment operator is a combination of a binary operator and the assignment (=) operator. Its syntax is -

x op = y -

where op represents the operator. Note that instead of the rvalue being replaced by the lvalue, the compound operator has the effect of writing -

x = x op y -

using the lvalue as the base for the result of the operation.

Note that I use the words "has the effect." The compiler doesn't literally translate something like x += 5 into x = x + 5. It just works that way logically. In fact, there's a major caveat to using an operation when the lvalue is a method. Let's examine that now: -

using System;
class CompoundAssignment1App
{
    protected int[] elements;
    public int[] GetArrayElement()
    {
        return elements;
    }
    CompoundAssignment1App()
    {
        elements = new int[1];
        elements[0] = 42;
    }
    public static void Main()
    {
        CompoundAssignment1App app = new CompoundAssignment1App();
        Console.WriteLine("{0}", app.GetArrayElement()[0]);
        app.GetArrayElement()[0] = app.GetArrayElement()[0] + 5;
        Console.WriteLine("{0}", app.GetArrayElement()[0]);
    }
}

Notice in the line in boldface-the call to the CompoundAssignment1App.- GetArrayElements method and subsequent modification of its first element-that I use the assignment syntax of -

x = x op y -

Here's the generated MSIL code: -

// Inefficient technique of x = x op y.
.method public hidebysig static void Main() il managed
{
  .entrypoint
  // Code size       79 (0x4f)
  .maxstack  4
  .locals (class CompoundAssignment1App V_0)
  IL_0000:  newobj     instance void CompoundAssignment1App::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldstr      "{0}"
  IL_000b:  ldloc.0
  IL_000c:  call       instance int32[] CompoundAssignment1App::GetArrayElement()
  IL_0011:  ldc.i4.0
  IL_0012:  ldelema    ['mscorlib']System.Int32
  IL_0017:  box        ['mscorlib']System.Int32
  IL_001c:  call       void ['mscorlib']System.Console::WriteLine
(class System.String,
 class System.Object)
  IL_0021:  ldloc.0
  IL_0022:  call
instance int32[] CompoundAssignment1App::GetArrayElement()
  IL_0027:  ldc.i4.0
  IL_0028:  ldloc.0
  IL_0029:  call
instance int32[] CompoundAssignment1App::GetArrayElement()
  IL_002e:  ldc.i4.0
  IL_002f:  ldelem.i4
  IL_0030:  ldc.i4.5
  IL_0031:  add
  IL_0032:  stelem.i4
  IL_0033:  ldstr      "{0}"
  IL_0038:  ldloc.0
  IL_0039:  call
instance int32[] CompoundAssignment1App::GetArrayElement()
  IL_003e:  ldc.i4.0
  IL_003f:  ldelema    ['mscorlib']System.Int32
  IL_0044:  box        ['mscorlib']System.Int32
  IL_0049:  call  void ['mscorlib']System.Console::WriteLine
                                 (class System.String, class System.Object)
  IL_004e:  ret
} // end of method. 'CompoundAssignment1App::Main'

Looking at the boldface lines in this MSIL, you can see that the Compound-Assignment1App.GetArrayElements method is actually being called twice! In a best-case scenario, this is inefficient. In the worst case, it could be disastrous, depending on what else the method does.

Now take a look at the following code, and note the change of the assignment to the compound assignment operator syntax: -

using System;
class CompoundAssignment2App
{
    protected int[] elements;
    public int[] GetArrayElement()
    {
        return elements;
    }
    CompoundAssignment2App()
    {
        elements = new int[1];
        elements[0] = 42;
    }
    public static void Main()
    {
        CompoundAssignment2App app = new CompoundAssignment2App();
        Console.WriteLine("{0}", app.GetArrayElement()[0]);
        app.GetArrayElement()[0] += 5;
        Console.WriteLine("{0}", app.GetArrayElement()[0]);
    }
}

The use of the compound assignment operator results in the following much more efficient MSIL code:-

// More efficient technique of x op= y.
.method public hidebysig static void Main() il managed
{
  .entrypoint
  // Code size       76 (0x4c)
  .maxstack  4
  .locals (class CompoundAssignment1App V_0, int32[] V_1)
  IL_0000:  newobj     instance void CompoundAssignment1App::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldstr      "{0}"
  IL_000b:  ldloc.0
  IL_000c:  call instance int32[] CompoundAssignment1App::GetArrayElement()
  IL_0011:  ldc.i4.0
  IL_0012:  ldelema    ['mscorlib']System.Int32
  IL_0017:  box        ['mscorlib']System.Int32
  IL_001c:  call   void ['mscorlib']System.Console::WriteLine
                                     (class System.String, class System.Object)
    IL_0021:  ldloc.0
    IL_0022:  call  instance int32[] CompoundAssignment1App::GetArrayElement()
    IL_0027:  dup
    IL_0028:  stloc.1
    IL_0029:  ldc.i4.0
    IL_002a:  ldloc.1
  IL_002b:  ldc.i4.0
  IL_002c:  ldelem.i4
  IL_002d:  ldc.i4.5
  IL_002e:  add
  IL_002f:  stelem.i4
  IL_0030:  ldstr      "{0}"
  IL_0035:  ldloc.0
  IL_0036:  call    instance int32[] CompoundAssignment1App::GetArrayElement()
  IL_003b:  ldc.i4.0
  IL_003c:  ldelema    ['mscorlib']System.Int32
  IL_0041:  box        ['mscorlib']System.Int32
  IL_0046:  call  void ['mscorlib']System.Console::WriteLine
                                     (class System.String, class System.Object)
  IL_004b:  ret
} // end of method 'CompoundAssignment1App::Main'

We can see that the MSIL dup opcode is being used. The dup opcode duplicates the top element on the stack, thereby making a copy of the value retrieved from the call to the CompoundAssignment1App.GetArrayElements method.

The point of this exercise has been to illustrate that although conceptually x += y is equivalent to x = x + y, subtle differences can be found in the generated MSIL. These differences mean you need to think carefully about which syntax to use in each circumstance. A basic rule of thumb, and my recommendation, is to use compound assignment operators whenever and wherever possible.