0

Does the method on the left side of the ?? operator in C# get called twice? Once for the evaluation and once for the assignment?

In the following line:

int i = GetNullableInt() ?? default(int);

I would assume that the GetNullableInt() method would need to be called first, so the result could be evaluated, before making the assignment. If this does NOT happen then the variable "i" would need to be assigned and then evaluated which seems dangerous for the item receiving the assignment in that, during an object assignment, it could theoretically be prematurely assigned a null value during the first stage only to have it replaced by the result of the method on the right.

?? Operator (C# Reference)

Ryan D'Baisse
  • 837
  • 11
  • 29
  • 2
    Why assume it would be called twice? - if it were, then it would be possible for the result to be wrong if the method returns a different value (which it would be entitled to) – Rowland Shaw Jun 18 '14 at 19:45
  • 13
    Seems like something you could test. – Anthony Pegram Jun 18 '14 at 19:45
  • I don't think so. It probably calls the method, assigns the return value to `i`, checks the if the value of `i` is `null`, and if yes - assigns the right part. That's how I would do it..... – Mario Stoilov Jun 18 '14 at 19:47
  • 2
    @MarioStoilov It will use an intermediate variable, but not necessarily `i`. While the runtime has a lot of options, generally this, (along with actually quite a lot of operations) results in an implicit unnamed temporary variable. After all, consider what would happen if the expression itself used `i` within it – Servy Jun 18 '14 at 19:49
  • 3
    @MarioStoilov: No, that's not what happens, and it *couldn't* be what happens - because `i` can never be null. The null-coalescing expression is *fully* evaluated, and the result assigned to `i`. – Jon Skeet Jun 18 '14 at 19:50
  • @Servy you're right, I didn't think about that. – Mario Stoilov Jun 18 '14 at 19:51

4 Answers4

11

There is a bug in the current C# compiler which will cause some aspects of evaluating the first operand to occur twice, in very specific situatoins - but no, GetNullableInt() will only be called once. (And the bug has been fixed in Roslyn.)

This is documented in the C# 5 specification in section 7.13, where each of the bullets in the list of options (based on what conversions are required) includes "At run-time, a is first evaluated." (a is the expression in the first operand.) It is only stated once, so it's only evaluated once. Note that the second operand is only called if it needs to be (i.e. if the first operand is null.)

Importantly, even if the type of i were int?, the assignment to i only happens after the expression to the right of the assignment operator is fully evaluated. It doesn't assign one value and then potentially assign a different one - it works out which value is going to be assigned, and then assigns it. This is how assignment always works. That becomes very important when there are conditional operators. For example:

Person foo = new Person();
foo = new Person { Spouse = foo };

That completely construts the new Person (assigning the old value of foo to its Spouse property) before assigning the reference to foo.

Community
  • 1
  • 1
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • As an aside, isn't `??` just syntactical sugar for `NUllable.GetValueOrDefault`? – Brad Christie Jun 18 '14 at 19:55
  • 3
    @BradChristie: No, not at all. For one thing, the second operand isn't evaluated unless the first operand is null, and also it applies to reference types as well. – Jon Skeet Jun 18 '14 at 19:56
7
namespace ConsoleApplication
{
    class Test
    {
        private static int count = 0;
        public static object TestMethod()
        {
            count++;
            return null;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var test = Test.TestMethod() ?? new object();
        }
    }
}

I just wrote up this test application. After running Test.TestMethod(), it looks like it's only incremented once, so it looks like it's only called once, regardless of whether TestMethod returns null or a new object.

Carlos Rodriguez
  • 2,190
  • 2
  • 18
  • 29
1

The first operand is only evaluated once, and the result is not assigned to the variable before the check for null.

The first operand is evalauted, then checked for null. If it isn't null, it becomes the value of the expression. If it is null, then the second operand is evaluated and used as the value of the exression. After that the value is assigned to the variable.

It's as if a temporary variable was used:

int? temp = GetNullableInt();
if (!temp.HasValue) temp = default(int);
int i = temp;
Guffa
  • 687,336
  • 108
  • 737
  • 1,005
0

I wrote this simple console app, putting the GetNullableInt() method in an external assembly to simplify things:

static int Main( string[] args )
{
  int i = SomeHelpers.GetNullableInt() ?? default(int) ;
  return i ;
}

Here's the IL as generated in different ways. You'll note that GetNullableInt() is only every called once, in all cases...at least for the usual case (can't speak to oddball edge conditions that might invoke a compiler bug). It would appear that the code

int i = GetNullableInt() ?? default(int) ;

is roughly equivalent to

int? t = GetNullableInt() ;
int  i = t.HasValue ? t.GetValueOrDefault() : 0 ;

Seems a little odd to me that the generated code

  1. First checks for a value, And then,
  2. Knowing in advance that the int? does in fact have a value, calls GetValueOrDefault() (implying an additional test of whether or not it's got a value), rather than simply referencing the Value property, but there you have it.

And what happens when that gets JIT'd, I know not.

Here's the MSIL:

  • Visual Studio 2010 SP1 (DEBUG):

    .method private hidebysig static int32  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       33 (0x21)
      .maxstack  2
      .locals init ([0] int32 i,
               [1] int32 CS$1$0000,
               [2] valuetype [mscorlib]System.Nullable`1<int32> CS$0$0001)
      IL_0000:  nop
      IL_0001:  call       valuetype [mscorlib]System.Nullable`1<int32> [SomeLibrary]SomeLibrary.SomeHelpers::GetNullableInt()
      IL_0006:  stloc.2
      IL_0007:  ldloca.s   CS$0$0001
      IL_0009:  call       instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
      IL_000e:  brtrue.s   IL_0013
      IL_0010:  ldc.i4.0
      IL_0011:  br.s       IL_001a
      IL_0013:  ldloca.s   CS$0$0001
      IL_0015:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
      IL_001a:  stloc.0
      IL_001b:  ldloc.0
      IL_001c:  stloc.1
      IL_001d:  br.s       IL_001f
      IL_001f:  ldloc.1
      IL_0020:  ret
    } // end of method Program::Main
    
  • Visual Studio 2010 SP1 (RELEASE)

    .method private hidebysig static int32  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       28 (0x1c)
      .maxstack  2
      .locals init ([0] int32 i,
               [1] valuetype [mscorlib]System.Nullable`1<int32> CS$0$0000)
      IL_0000:  call       valuetype [mscorlib]System.Nullable`1<int32> SomeLibrary]SomeLibrary.SomeHelpers::GetNullableInt()
      IL_0005:  stloc.1
      IL_0006:  ldloca.s   CS$0$0000
      IL_0008:  call       instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
      IL_000d:  brtrue.s   IL_0012
      IL_000f:  ldc.i4.0
      IL_0010:  br.s       IL_0019
      IL_0012:  ldloca.s   CS$0$0000
      IL_0014:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
      IL_0019:  stloc.0
      IL_001a:  ldloc.0
      IL_001b:  ret
    } // end of method Program::Main
    
  • Visual Studio 2013 (DEBUG)

    .method private hidebysig static int32  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       34 (0x22)
      .maxstack  1
      .locals init ([0] int32 i,
               [1] int32 CS$1$0000,
               [2] valuetype [mscorlib]System.Nullable`1<int32> CS$0$0001)
      IL_0000:  nop
      IL_0001:  call       valuetype [mscorlib]System.Nullable`1<int32> [SomeLibrary]SomeLibrary.SomeHelpers::GetNullableInt()
      IL_0006:  stloc.2
      IL_0007:  ldloca.s   CS$0$0001
      IL_0009:  call       instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
      IL_000e:  brtrue.s   IL_0013
      IL_0010:  ldc.i4.0
      IL_0011:  br.s       IL_001a
      IL_0013:  ldloca.s   CS$0$0001
      IL_0015:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
      IL_001a:  nop
      IL_001b:  stloc.0
      IL_001c:  ldloc.0
      IL_001d:  stloc.1
      IL_001e:  br.s       IL_0020
      IL_0020:  ldloc.1
      IL_0021:  ret
    } // end of method Program::Main
    
  • Visual Studio 2013 (RELEASE)

    .method private hidebysig static int32  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       28 (0x1c)
      .maxstack  1
      .locals init ([0] int32 i,
               [1] valuetype [mscorlib]System.Nullable`1<int32> CS$0$0000)
      IL_0000:  call       valuetype [mscorlib]System.Nullable`1<int32> [SomeLibrary]SomeLibrary.SomeHelpers::GetNullableInt()
      IL_0005:  stloc.1
      IL_0006:  ldloca.s   CS$0$0000
      IL_0008:  call       instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
      IL_000d:  brtrue.s   IL_0012
      IL_000f:  ldc.i4.0
      IL_0010:  br.s       IL_0019
      IL_0012:  ldloca.s   CS$0$0000
      IL_0014:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
      IL_0019:  stloc.0
      IL_001a:  ldloc.0
      IL_001b:  ret
    } // end of method Program::Main
    
Nicholas Carey
  • 71,308
  • 16
  • 93
  • 135