I've had a search and haven't found a good explanation for why the following occurs.
I have two classes which have an interface in common and I have tried initializing an instance of this interface type using the ternary operator as below but this fails to compile with the error "Type of conditional expression cannot be determined because there is no implicit conversion between 'xxx.Class1' and 'xxx.Class2':

The alternatives are fine but I can't quite get my head around why the first option fails with the implicit conversion error as, in my view, both classes are of type ILogger and I am not really looking to do a conversion (implicit or explicit). I'm sure this is probably a static language compilation issue but I would like to understand what is going on.

4 Answers
4

The first is that C# never "magics up" a type for you. If C# must determine a "best" type from a given set of types, it always picks one of the types you gave it. It never says "none of the types you gave me are the best type; since the choices you gave me are all bad, I'm going to pick some random thing that you did not give me to choose from."

The second is that C# reasons from inside to outside. We do not say "Oh, I see you are trying to assign the conditional operator result to an ILogger; let me make sure that both branches work." The opposite happens: C# says "let me determine the best type returned by both branches, and verify that the best type is convertible to the target type."

The second rule is sensible because the target type might be what we are trying to determine. When you say D d = b ? c : a; it is clear what the target type is. But suppose you were instead calling M(b?c:a)? There might be a hundred different overloads of M each with a different type for the formal parameter! We have to determine what the type of the argument is, and then discard overloads of M which are not applicable because the argument type is not compatible with the formal parameter type; we don't go the other way.

Consider what would happen if we went the other way:

M1( b1 ? M2( b3 ? M4( ) : M5 ( ) ) : M6 ( b7 ? M8() : M9() ) );

Suppose there are a hundred overloads each of M1, M2 and M6. What do you do? Do you say, OK, if this is M1(Foo) then M2(...) and M6(...) must be both convertible to Foo. Are they? Let's find out. What's the overload of M2? There are a hundred possibilities. Let's see if each of them is convertible from the return type of M4 and M5... OK, we've tried all those, so we've found an M2 that works. Now what about M6? What if the "best" M2 we find is not compatible with the "best" M6? Should we backtrack and keep on re-trying all 100 x 100 possibilities until we find a compatible pair? The problem just gets worse and worse.

We do reason in this manner for lambdas and as a result overload resolution involving lambdas is at least NP-HARD in C#. That is bad right there; we would rather not add more NP-HARD problems for the compiler to solve.

You can see the first rule in action in other place in the language as well. For example, if you said: ILogger[] loggers = new[] { consoleLogger, suppressLogger }; you'd get a similar error; the inferred array element type must be the best type of the typed expressions given. If no best type can be determined from them, we don't try to find a type you did not give us.

Same thing goes in type inference. If you said:

void M<T>(T t1, T t2) { ... }
...
M(consoleLogger, suppressLogger);

Then T would not be inferred to be ILogger; this would be an error. T is inferred to be the best type amongst the supplied argument types, and there is no best type amongst them.

@Eric Lippert - thank you, this is a wonderfully detailed answer and I now understand the cause and the reasons behind the behaviour. I look forward to checking out your links.
–
Andy RoseNov 17 '11 at 8:54

I don't understand how the M<T>()case relates to the ILogger[] = new [] {} case or the conditional operator. In the first case, the compiler is left on its own to figure things out. I understand why that's hard. But in the other cases the expected type is given, so why force the compiler to find the best type? Isn't the problem reduced to figure out if the given types are compatible?
–
Thomas EydeDec 25 '14 at 12:24

@ThomasEyde: You ask "why force the compiler to find the best type?" Because it is much easier to specify the behaviour of the language when the rules for t = x and M(x) are the same: you work out what x is and then see if that is compatible with the context. Changing the rules depending on the context would mean that what are today simple, safe refactorings would suddenly start changing the meaning of the program.
–
Eric LippertDec 27 '14 at 15:21

Ok, I think I understand how the current rules are easier to implement. But I don't get your last comment, maybe we are thinking of different things? If we let the compiler account for the result type when given, the underlying type of the expression will not change. How can that change the meaning of the program?
–
Thomas EydeDec 28 '14 at 2:15

@ThomasEyde: If there are subtly different rules for resolving the types of b and c in T x = a ? b : c; M(x); and M(a ? b : c) then refactoring one into the other could change the meaning of the program, which is very unexpected. Basically any time you want the compiler to deduce a fact from context, you introduce a breaking change on refactorings that change context.
–
Eric LippertDec 28 '14 at 16:45

When you have an expression like condition ? a : b, there must be an implicit conversion from the type of a to the type of b, or the other way round, otherwise the compiler can't determine the type of the expression. In your case, there is no conversion between SuppressLogger and ConsoleLogger...

Also, it would also fail if an implicit conversion exists in both directions, which in this case also prevents the compiler from determining the "correct" one.
–
anonymousenNov 16 '11 at 17:12

1

Ok, I think I see where my confusion lies. I think I was under the impression that the type I was expecting the condition to return was implied by the type I was assigning it to but this is obviously not the case. The condition is self contained and, as you point out, the possible results must have an implicit conversion between them to compile. The result of the condition will then be implicitly assigned to my variable.
–
Andy RoseNov 16 '11 at 17:14

@AndyRose, I added a reference to the relevant section of the specifications, if you want to look it up
–
Thomas LevesqueNov 16 '11 at 17:17

Note that you only need to case one of the two legs of the ternary condition, in case you think the shorter version is prettier than the symmetrical version.
–
jwgMar 13 '13 at 15:48

Anyone knows why the compiler ignores the assigned type? Wouldn't that be the obvious thing to do? Isn't that what the compiler did before we got var? The following looks so wrong to me: object o = b ? (object) 1 : "two". Everything is assignable to object, but I still need an explicit cast.
–
Thomas EydeDec 25 '14 at 12:33

Any time you change a variable of one type into a variable of another type, that's a conversion. Assigning an instance of a class to a variable of any type other than that class requires a conversion. This statement:

ILogger a = new ConsoleLogger();

will perform an implicit conversion from ConsoleLogger to ILogger, which is legal because ConsoleLogger implements ILogger. Similarly, this will work:

because there is an implicit conversion between SuppressLogger and ILogger. However, this won't work:

ILogger c = suppress ? new SuppressLogger() : new ConsoleLogger();

because the tertiary operator will only try so hard to figure out what type you wanted in the result. It essentially does this:

If the types of operands 2 and 3 are the same, the tertiary operator returns that type and skips the rest of these steps.

If operand 2 can be implicitly converted to the same type as operand 3, it might return that type.

If operand 3 can be implicitly converted to the same type as operand 2, it might return that type.

If both #2 and #3 are true, or neither #2 or #3 are true, it generates an error.

Otherwise, it returns the type for whichever of #2 or #3 was true.

In particular, it will not start searching through all the types it knows about looking for a "least common denominator" type, such as an interface in common. Also, the tertiary operator is evaluated, and its return type deterimined, independant of the type of variable you are storing the result into. It's a two step process:

Determine the type of the ?: expression and calculate it.

Store the result of #1 into the variable, performing any implicit conversions as needed.

Typecasting one or both of your operands is the correct way to perform this operation if that's what you need.