61

I'm a little stumped by this little C# quirk:

Given variables:

Boolean aBoolValue;
Byte aByteValue;

The following compiles:

if (aBoolValue) 
    aByteValue = 1; 
else 
    aByteValue = 0;

But this will not:

aByteValue = aBoolValue ? 1 : 0;

Error says: "Cannot implicitly convert type 'int' to 'byte'."

And of course, this monstrosity will compile:

aByteValue = aBoolValue ? (byte)1 : (byte)0;

What's going on here?

EDIT:

Using VS2008, C# 3.5

MPelletier
  • 16,256
  • 15
  • 86
  • 137
  • possible duplicate of [Nullable types and the ternary operator: why is \`? 10 : null\` forbidden?](http://stackoverflow.com/questions/858080/nullable-types-and-the-ternary-operator-why-is-10-null-forbidden) – nawfal Apr 20 '13 at 00:41

3 Answers3

70

This is a fairly frequently asked question.

In C#, we almost always reason from inside to outside. When you see

x = y;

we work out what is the type of x, what is the type of y, and whether the type of y is assignment compatible with x. But we do not use the fact that we know what the type of x is when we are working out the type of y.

That's because there might be more than one x:

void M(int x) { }
void M(string x) { }
...
M(y); // y is assigned to either int x or string x depending on the type of y

We need to be able to work out the type of an expression without knowing what it is being assigned to. Type information flows out of an expression, not into an expression.

To work out the type of the conditional expression, we work out the type of the consequence and the alternative expressions, pick the more general of the two types, and that becomes the type of the conditional expression. So in your example, the type of the conditional expression is "int", and it is not a constant (unless the condition expression is constant true or constant false). Since it is not a constant, you can't assign it to byte; the compiler reasons solely from the types, not from the values, when the result is not a constant.

The exception to all these rules is lambda expressions, where type information does flow from the context into the lambda. Getting that logic right was very difficult.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 2
    My mind is blown. Thank you, sir, that is one part enlightening and two parts frustrating. One because it can't work the way it would *seem* to work, and two because it makes sense that it can't work that way. So... constants it is! – MPelletier Feb 07 '10 at 05:21
  • Thanks. I was looking for a post on this topic on your blog, but this is even better. – John Knoeller Feb 07 '10 at 06:31
  • 1
    @John: I talk about these issues a bit here: http://blogs.msdn.com/ericlippert/archive/2006/05/24/type-inference-woes-part-one.aspx – Eric Lippert Feb 07 '10 at 15:11
  • 2
    @Eric Lippert, I attempted to comment on your blog entry, but I'm not sure it took, so I'll ask here: Rather than attempt to resolve the types indepdently, why doesn't the compiler just convert the entire ternary expression into the equivalent "verbose expression" (in this case, `byte aByteValue; if (aBoolValue) aByteValue = 1; else aByteValue = 0;`)? Is there some gotcha that would make this not viable? Or is it a philosophical thing (e.g., "If it looks like an expression, it should be evaluated like one.")? I think most programmers would initially expect ternary to work exactly like if-else. – devuxer Jan 27 '12 at 02:37
  • 1
    `var x = y` is different from `int x = y` and should be treated differently. In the first case I ask the compiler to figure this out, in the latter I tell it what I expect. And, as @devuxer says, most developers expect `?:` to work like `if-else`. – Thomas Eyde Dec 25 '14 at 16:32
  • As of 2021 Eric's article can be found here: https://learn.microsoft.com/en-us/archive/blogs/ericlippert/type-inference-woes-part-one – Andriy K Jun 09 '21 at 16:45
12

I'm using VS 2005, for and I can reproduce, for bool & Boolean, but not for true

 bool abool = true;
 Boolean aboolean = true;
 Byte by1 = (abool ? 1 : 2);    //Cannot implicitly convert type 'int' to 'byte'
 Byte by2 = (aboolean ? 1 : 2); //Cannot implicitly convert type 'int' to 'byte'
 Byte by3 = (true ? 1 : 2);     //Warning: unreachable code ;)

The simplest workaround seems to be this cast

 Byte by1 = (Byte)(aboolean ? 1 : 2);

So, yes, it seems that the ternary operator is causing the constants to "fix" their types as ints and disable the implicit type conversion that you would otherwise get from constants that fit within the smaller type.

John Knoeller
  • 33,512
  • 4
  • 61
  • 92
  • 1
    Your explanation makes sense. But why would the constants not fix in this circumstance? And doubly curious with Mendy's discovery. Someone at Microsoft should know, and might need to be argued with strongly... – MPelletier Feb 07 '10 at 03:37
  • I think Mendy discovered that when compiler can trivially detect that it can optimize the ternary operator away entirely, it the compiles code that is equal to `Byte by = 2` which preserves the ability to implicitly cast. (see my comment on the compiler warning above) – John Knoeller Feb 07 '10 at 03:43
  • I think Eric Lippert may have discussed the why of this already on his blog. – John Knoeller Feb 07 '10 at 03:44
  • @John Knoeller: can you find link? – Fitzchak Yitzchaki Feb 07 '10 at 03:47
  • Heres a related link to his blog. http://blogs.msdn.com/ericlippert/archive/2009/03/19/representation-and-identity.aspx I havn't found the exact link, and I may have mis-remembered where I read about implicit casts anyway. – John Knoeller Feb 07 '10 at 03:53
  • 1
    @Mendy: Found it http://blogs.msdn.com/ericlippert/archive/2006/05/24/type-inference-woes-part-one.aspx – John Knoeller Feb 07 '10 at 19:59
7

I may not have a great answer for you, but if you do this in many places, you could declare:

private static readonly Byte valueZero = (byte)0;
private static readonly Byte valueOne = (byte)1;

and just these variables. You might get away with using const if it is local to the project.

EDIT: using readonly would not make sense - these aren't ever meant to change.

Hamish Grubijan
  • 10,562
  • 23
  • 99
  • 147