-1

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)

6
  • 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) Commented Jun 18, 2014 at 19:45
  • 13
    Seems like something you could test. Commented Jun 18, 2014 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..... Commented Jun 18, 2014 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 Commented Jun 18, 2014 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. Commented Jun 18, 2014 at 19:50

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

2 Comments

As an aside, isn't ?? just syntactical sugar for NUllable<T>.GetValueOrDefault?
@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.
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.

Comments

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; 

Comments

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 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.