Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Null coalesce operator implications?

Tags:

A while back I compiled two versions of a code, one using (Nullable<T>)x.GetValueOrDefault(y) and one using (Nullable<T>)x ?? y).

After decompiling to IL I noticed that the null coalesce operator is transformed into the GetValueOrDefault call.

Since it's a method call to which an expression can be passed that is evaluated before execution of the method, y seems to be always executed.

For example:

using System;

public static class TestClass
{
    private class SomeDisposable : IDisposable
    {
        public SomeDisposable()
        {
            // Allocate some native resources
        }

        private void finalize()
        {
            // Free those resources
        }

        ~SomeDisposable()
        {
            finalize();
        }

        public void Dispose()
        {
            finalize();
            GC.SuppressFinalize(this);
        }
    }

    private struct TestStruct
    {
        public readonly SomeDisposable _someDisposable;
        private readonly int _weirdNumber;

        public TestStruct(int weirdNumber)
        {
            _weirdNumber = weirdNumber;
            _someDisposable = new SomeDisposable();
        }
    }

    public static void Main()
    {
        TestStruct? local = new TestStruct(0);

        TestStruct local2 = local ?? new TestStruct(1);

        local2._someDisposable.Dispose();
    }
}

Seems to result into an indisposed object, and probably performance implications too.

First of all, is this true? Or does the JIT or something alike change the actually executed ASM code?

And secondly can someone explain why it has this behavior?

NOTE: This is just an example, it is not based upon real code, and please refrain from making comments like 'this is bad code'.

IL DASM:
Okay, when I compiled this with .Net Framework 2.0 it resulted in identical code with calling null coalesce and GetValueOrDefault. With .Net Framework 4.0, it generates these two codes:

GetValueOrDefault:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] valuetype [mscorlib]System.Nullable`1<int32> nullableInt,
           [1] int32 nonNullableInt)
  IL_0000:  nop
  IL_0001:  ldloca.s   nullableInt
  IL_0003:  initobj    valuetype [mscorlib]System.Nullable`1<int32>
  IL_0009:  ldloca.s   nullableInt
  IL_000b:  ldc.i4.1
  IL_000c:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault(!0)
  IL_0011:  stloc.1
  IL_0012:  ret
} // end of method Program::Main

Null Coalesce:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       32 (0x20)
  .maxstack  2
  .locals init (valuetype [mscorlib]System.Nullable`1<int32> V_0,
           int32 V_1,
           valuetype [mscorlib]System.Nullable`1<int32> V_2)
  IL_0000:  nop
  IL_0001:  ldloca.s   V_0
  IL_0003:  initobj    valuetype [mscorlib]System.Nullable`1<int32>
  IL_0009:  ldloc.0
  IL_000a:  stloc.2
  IL_000b:  ldloca.s   V_2
  IL_000d:  call       instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
  IL_0012:  brtrue.s   IL_0017
  IL_0014:  ldc.i4.1
  IL_0015:  br.s       IL_001e
  IL_0017:  ldloca.s   V_2
  IL_0019:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
  IL_001e:  stloc.1
  IL_001f:  ret
} // end of method Program::Main

As it turns out that this is no longer the case, and that it skips over the GetValueOrDefault call altogether when HasValue returns false.