5

I just encountered this (made up code to demonstrate the "problem"):

public ICollection<string> CreateCollection(int x)
{
    ICollection<string> collection = x == 0 
                                   ? new List<string>() 
                                   : new LinkedList<string>();
    return collection;
}

The compiler complains:

Fehler CS0173: Der Typ des bedingten Ausdrucks kann nicht bestimmt werden, weil keine implizite Konvertierung zwischen "System.Collections.Generic.List" und "System.Collections.Generic.LinkedList" erfolgt.

Which translates roughly to:

The type of the conditional operator can not be determined, because there is no implicit conversion between List and LinkedList.

I can see why the compiler complains, but hey, come on. It is trying to play stupid. I can see that both expressions are not of the same type but have one common ancestor and as a bonus the type of the left side is also a common ancestor. I am sure the compiler can see it too. I could understand the error if the left side was declared as var.

What am I missing here?

Edit:

I am accepting James Gaunt's explanation. Maybe Just to make it clear. I can read the compiler spec just fine. I wanted to understand why. Why did someone make the decision to write the spec that way. There must be a reason behind that design. According to James the design principle is 'no surprises'. Also CodeInChaos explains what surprises you might encounter if the compiler would try to deduce the type from common ancestors.

EricSchaefer
  • 25,272
  • 21
  • 67
  • 103
  • 1
    Try sticking an explicit cast to `ICollection` on one of the conditional results. – Anon. Dec 12 '10 at 21:48
  • I know how to circumvent it, I just want to understand it. – EricSchaefer Dec 12 '10 at 21:50
  • what if... the compiler would create an anonymous type that would (A) implement all the common interfaces (B) act as an wrapper for the selected object? – m0sa Dec 12 '10 at 22:20
  • @m0sa That wouldn't work well because suddenly the result pointed to a different instance. – CodesInChaos Dec 12 '10 at 22:33
  • 1
    A machine is complaining to you. I'm pretty sure you are smarter than that machine, you know what to do. The rule in the language spec is just as mechanical, it stops you from aiming a gun at your foot and pulling the trigger. It doesn't blow your foot off, it blows up in your face. – Hans Passant Dec 12 '10 at 23:08
  • I am not complaining about the complaining. I want to understand the reason. – EricSchaefer Dec 13 '10 at 07:36

4 Answers4

10

The expression (a ? b : c) has to resolve to a type. The type will be either the type of b or c. If these are different (and there isn't an implicit conversion from one to the other) the compiler doesn't know which type this is at compile time.

You may say it should deduce that there is common root type, but there is always a common root type (e.g. Object).

In general the C# compiler will not attempt to guess what you mean. If you want to use the common root type then cast b and c to that type.

This kind of logic runs throughout the design of C#, it is occasionally a bit annoying, but far more often it stops you making mistakes.

James Gaunt
  • 14,631
  • 2
  • 39
  • 57
4

Because of interfaces they can have multiple different common ancestors.

One could add a requirement that it only auto-converts if the ancestor is unambiguous. But then adding additional interfaces a class implements suddenly becomes a breaking change. And that might not be desirable.

For example suppose you make these types implement ISerializeable. This shouldn't change the behavior of your code, but if you supported that casting to common interface it would.

edit: Thought a bit more about it and noticed that this function already has exactly the same problem:

T MyFunc<T>(T left,T right)

And this code doesn't compile:

ICollection<string> r=MyFunc(new List<string>() , new LinkedList<string>());

because it can't decide which type to use as the type-parameter T. So the behavior of the ?: operator is consistent with overload resolution.

CodesInChaos
  • 106,488
  • 23
  • 218
  • 262
2

The left-hand side isn't taken into consideration at all when determining the type of the right-hand side.

It's only when the compiler has independently determined the right-hand side's type that it checks for assignment compatibility with the left-hand side.

As for your claim that both types "have one common ancestor": would that be ICollection, IEnumerable, ICollection<T>, IEnumerable<T> or Object? What heuristic should the compiler use to unambiguously determine which type you want? The compiler is simply asking you to specify, rather than trying to guess your intention.

LukeH
  • 263,068
  • 57
  • 365
  • 409
  • But still do both expressions on the right side have an common ancestor (type-wise). – EricSchaefer Dec 12 '10 at 21:51
  • they have many common ancestors, which makes the compiler throw an error... would you expect this to work as well? (var obj = true ? 1 : "0"), what type would obj be now? according to your logic it would be of type Object, but thats pretty far fetched. – Pauli Østerø Dec 12 '10 at 22:16
  • Well, at least the compiler could try to be smart about it. As a customer always complains: "Why do I have to do that, let the computer do that..." ;-) – EricSchaefer Dec 13 '10 at 07:30
  • @Eric: If the compiler tried to be smart then it would probably get it wrong in a significant number of cases. Which would you prefer: a compiler that silently produces incorrect logic half of the time, or a compiler that acts dumb but always generates the correct code? – LukeH Dec 13 '10 at 09:59
  • I know that. The F# compiler IS smart about it and it does figure it out correctly. I don't know if the F# syntax forbids any ambiguous situations... – EricSchaefer Dec 15 '10 at 13:12
2

It simply is the definition of ?: to require equal types.
You could of course use

? (ICollection<string>) new List<string>() 
: (ICollection<string>) new LinkedList<string>();

Or just use an if/else.

According to the C# reference, § 14.13,

[Given] A conditional expression of the form b ? x : y

  • If X and Y are the same type, then this is the type of the conditional expression.
  • Otherwise, if an implicit conversion (§13.1) exists from X to Y, but not from Y to X, then Y is the type of the conditional expression.
  • Otherwise, if an implicit conversion (§13.1) exists from Y to X, but not from X to Y, then X is the type of the conditional expression.
  • Otherwise, no expression type can be determined, and a compile-time error occurs.

In your case, both X and Y have a conversion to Z, but that doesn't help. It is a general principle in the language that the compiler does not even look at the target variable when applying the rules. Simple example: double a = 7 / 2; // a becomes 3.0

So after reading this I thing it would suffice to cast just 1 of the results to ICollection<string>. I didn't test that.

H H
  • 263,252
  • 30
  • 330
  • 514