Hi, I got a strange issue today I posted on stackoverflow.
The Issue
Here’s the situation, I’ve abstracted the name and logic to focus on the issue. Got 3 types, A, B and C. B & C have implicit operators defined to convert to A object.
public class A { public static implicit operator A(B input){ /* Convert B to A */ } public static implicit operator A(C input) { /* Convert C to A*/ } } public class B { } public class C { }
Then, when I do this, the code compile and work fine :
A myObject = null; if (condition) myObject = new B(); else myObject = new C();
But when I write the same logic with an inline if, I got an error :
A myObject = condition ? new B() : new C();
Type of conditional expression cannot be determined because there is no implicit conversion between 'B' and 'C'
WHY ? Why does it work in classical if statement and not in inline if statement ?
The Answer
Here’s his answer :
Absolutely. The type of a conditional operator expression must either be the type of the second operand or the type of the third operand. (And if those two types aren’t the same, exactly one of the types has to be implicitly convertible to the other.) The compiler doesn’t try to find a “lower common denominator” type, and the use of the result isn’t important either (the compiler doesn’t “notice” that you’re assigning the result to a variable of type A
).
You can fix this yourself by just explicitly casting either operand:
A myObject = condition ? (A) new B() : new C();
or
A myObject = condition ? new B() : (A) new C();
Note that this isn’t limited to user-defined conversion operators; the same is true for simple reference conversions based on derived classes:
Button x = new Button(); String y = "foo"; object z = condition ? x : y; // Compile-time error
See section 7.14 of the C# specification for more details.
Behind the scene
But the answer wasn’t totally complete for me. So I took my lovely decompiler and anaylyze what’s generated in the case of an inline and classical if. The code is “self explanatory” and I’ve commented each MSIL instruction to make it readable.
static void ClassicalIf(bool condition) { int i = 0; if (condition) i = 1; else i = 2; } static void InlineIf(bool condition) { int i = condition ? 1 : 2; }
For the inline if :
.method private hidebysig static void InlineIf(bool condition) cil managed { .maxstack 1 .locals init ( [0] int32 i) L_0000: nop L_0001: ldarg.0 -- Load argument '0' onto the stack L_0002: brtrue.s L_0007 -- Branch to L_0007 if value is non-zero L_0004: ldc.i4.2 -- Push 2 onto the stack L_0005: br.s L_0008 -- Branch to L_0008 L_0007: ldc.i4.1 -- Push 1 onto the stack L_0008: nop L_0009: stloc.0 -- Pop from stack into local variable 0 L_000a: ret }
And here’s the one for the “normal” if :
.method private hidebysig static void ClassicalIf(bool condition) cil managed { .maxstack 2 .locals init ( [0] int32 i, [1] bool CS$4$0000) -- Additional bool for if L_0000: nop L_0001: ldc.i4.0 -- Push 0 onto the stack L_0002: stloc.0 -- Pop from stack into local variable '0' L_0003: ldarg.0 -- Load argument '0' onto the stack L_0004: ldc.i4.0 -- Push 0 onto the stack L_0005: ceq -- Push 1 if value1 equals value2 (on stack), else push 0. L_0007: stloc.1 -- Pop from stack into local variable '1' L_0008: ldloc.1 -- Load local variable '1' onto stack. L_0009: brtrue.s L_000f -- Branch to L_000f if value is non-zero L_000b: ldc.i4.1 -- Push 1 onto the stack L_000c: stloc.0 -- Pop from stack into local variable '0' L_000d: br.s L_0011 -- Branch to L_0011 L_000f: ldc.i4.2 -- Push 2 onto the stack L_0010: stloc.0 -- Pop from stack into local variable '0' L_0011: ret }
Now same thing with an additional cast :
static void ClassicalIf(bool condition) { object i = 0; if (condition) i = new object(); else i = 2; } static void InlineIf(bool condition) { object i = condition ? new object() : 2; }
For the inline if :
.method private hidebysig static void InlineIf(bool condition) cil managed { .maxstack 1 .locals init ( [0] object i) L_0000: nop L_0001: ldarg.0 -- Load argument '0' onto the stack L_0002: brtrue.s L_000c -- Branch to L_000c if value is non-zero (true) L_0004: ldc.i4.2 -- Push 2 onto the stack L_0005: box int32 -- Boxing L_000a: br.s L_0011 -- Branch to L_0008 L_000c: newobj instance void-- Create new object L_0011: nop L_0012: stloc.0 -- Pop from stack into local variable 0 L_0013: ret }
And here’s the one for the “normal” if :
.method private hidebysig static void ClassicalIf(bool condition) cil managed { .maxstack 2 .locals init ( [0] object i, [1] bool CS$4$0000) L_0000: nop -- Push 0 onto the stack L_0001: ldc.i4.0 -- Pop from stack into local variable '0' L_0002: box int32 -- Boxing L_0007: stloc.0 -- Pop from stack into local variable 0 L_0008: ldarg.0 -- Load argument '0' onto the stack L_0009: ldc.i4.0 -- Push 0 onto the stack L_000a: ceq -- Push 1 if value1 equals value2 (on stack), else push 0. L_000c: stloc.1 -- Pop from stack into local variable '1' L_000d: ldloc.1 -- Load local variable '1' onto stack. L_000e: brtrue.s L_0018 -- Branch to L_0018 if value is non-zero (true) L_0010: newobj instance void [ms-- Create new object L_0015: stloc.0 -- Pop from stack into local variable '0' L_0016: br.s L_001f -- Branch to L_001f L_0018: ldc.i4.2 -- Push 2 onto the stack L_0019: box int32 -- Boxing L_001e: stloc.0 -- Pop from stack into local variable '0' L_001f: ret }
Conclusion
1) So I will also try to use inline if as possible. Less instructions (so CPU) and less memory usage.
2) Also it’s now crystal clear when we see how an inline if is converted to MSIL 🙂
Catch you next time and keep it bug free !
The engaging nature of distinct games serves to make
such games well-known in common but at the
very same time, games that are much more engaging are typically far more popular.