简体   繁体   中英

null coalescing issue with abstract base/derived classes

Why is the C# null coalescing operator not able to figure this out?

  Cat c = new Cat();
  Dog d = null;

  Animal a = d ?? c;

This will give the error

Operator ?? cannot be applied to operands of type Dog and Cat

It just seems strange given the following compiles.

Animal a = d;
a = c;

Contextual code below:

public abstract class Animal
{
  public virtual void MakeNoise()
  {        
    Console.WriteLine("noise");
  }    
}

public class Dog : Animal
{
  public override void MakeNoise()
  {
     Console.WriteLine("wuff");
  }
}

public class Cat : Animal
{
  public override void MakeNoise()
  {
    Console.WriteLine("miaow");
  }
}

One of the subtle design rules of C# is that C# never infers a type that wasn't in the expression to begin with. Since Animal is not in the expression d ?? c d ?? c , the type Animal is not a choice.

This principle applies everywhere that C# infers types. For example:

var x = new[] { dog1, dog2, dog3, dog4, cat }; // Error

The compiler does not say "this must be an array of animals", it says "I think you made a mistake".

This is then a specific version of the more general design rule, which is "give an error when a program looks ambiguous rather than making a guess that might be wrong".

Another design rule that comes into play here is: reason about types from inside to outside, not from outside to inside. That is, you should be able to work out the type of everything in an expression by looking at its parts , without looking at its context . In your example, Animal comes from outside the ?? expression; we should be able to figure out what the type of the ?? expression is and then ask the question "is this type compatible with the context?" rather than going the other way and saying "here's the context -- now work out the type of the ?? expression."

This rule is justified because very often the context is unclear. In your case the context is very clear; the thing is being assigned to Animal . But what about:

var x = a ?? b;

Now the type of x is being inferred. We don't know the type of the context because that's what we're working out. Or

M(a ?? b)

There might be two dozen overloads of M and we need to know which one to pick based on the type of the argument. It is very hard to reason the other way and say "the context could be one of these dozen things; evaluate a??b in each context and work out its type".

That rule is violated for lambdas, which are analyzed based on their context. Getting that code both correct and efficient was very difficult; it took me the better part of a year's work. The compiler team can do more features, faster and better, by not taking on that expense where it is not needed.

Before assigning it to Animal a , c and d are still Cat and Dog respectively. The following does work the way you'd expect:

Animal a = (Animal)c ?? (Animal)d;

For the same reason that Animal a = (true)? d : c; Animal a = (true)? d : c; won't work (using the ternary operator).

According to the C# specification, the type of the expression is inferred as follows (quoting Eric Lippert ):

The second and third operands of the ?: operator control the type of the conditional expression. Let X and Y be the types of the second and third operands. Then,

  • If X and Y are the same type, then this is the type of the conditional expression.
  • Otherwise, if an implicit conversion 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 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.

Since no implicit conversion exists from Dog to Cat, nor from Cat to Dog, then the type can't be inferred. The same principle applies to the null coalescing operator.

Edit

Why does null coalesce care about the relationship between Cat and Dog and not just about the relationship between Cat and Animal and Dog and Animal?

As to why the compiler doesn't just realize that both operators are Animal s:

It's just too big a can of worms. We like the principle that the type of the expression must be the type of something in the expression.

It fails because c cannot be converted to d implicitly and vice-versa. Obvoiously Cat is not a Dog and Dog is not a Cat either.

Try this

Animal a = (Animal)d ?? c;

Now we say the compiler that left hand side operand of ?? is Animal and yes it can convert "dog to animal" and also right and side operand of ?? is Cat that is also can be converted to "Animal". Compiler is happy now :)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM