Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implicit conversion issue in a ternary condition [duplicate]

Possible Duplicate:
Conditional operator cannot cast implicitly?
Why does null need an explicit type cast here?

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':

public ConsoleLogger : ILogger  { .... }

public SuppressLogger : ILogger  { .... }

static void Main(string[] args)
{
   .....
   // The following creates the compile error
   ILogger logger = suppressLogging ? new SuppressLogger() : new ConsoleLogger();
}

This works if I explicitly cast the first conditioin to my interface:

   ILogger logger = suppressLogging ? ((ILogger)new SuppressLogger()) : new ConsoleLogger();

and obviously I can always do this:

   ILogger logger;
   if (suppressLogging)
   {
       logger = new SuppressLogger();
   }
   else
   {
       logger = new ConsoleLogger();
   }

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.

like image 909
Andy Rose Avatar asked Nov 16 '11 17:11

Andy Rose


3 Answers

This is a consequence of the confluence of two characteristics of C#.

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.

For more details on how this design decision influences the behaviour of the conditional operator, see my series of articles on that topic.

If you are interested in why overload resolution that works "from outside to inside" is NP-HARD, see this article.

like image 166
Eric Lippert Avatar answered Oct 30 '22 23:10

Eric Lippert


You can do that:

ILogger logger = suppressLogging ? (ILogger)(new SuppressLogger()) : (ILogger)(new ConsoleLogger());

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...

(see section 7.14 in the C# 4 language specifications for details)

like image 38
Thomas Levesque Avatar answered Oct 30 '22 23:10

Thomas Levesque


The problem is that the right hand side of the statement is evaluated without looking at the type of the variable it is assigned to.

There's no way the compiler can look at

suppressLogging ? new SuppressLogger() : new ConsoleLogger();

and decide what the return type should be, since there's no implicit conversion between them. It doesn't look for common ancestors, and even if it did, how would it know which one to pick.

like image 3
Ray Avatar answered Oct 31 '22 01:10

Ray