null coalescing issue with abstract base/derived classes
Asked Answered
F

4

16

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");
  }
}
Forgot answered 14/11, 2013 at 10:2 Comment(0)
S
22

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

Stiff answered 14/11, 2013 at 15:49 Comment(1)
I think I understand why the ?? can't infer the common base class, but I have to nitpick a little; readers should take note that adding the required extra casts as hints to ?? does defeat the type system a little will increase the chance of runtime failure. For example, if you did Animal a = d ?? (Animal) c; and c was something more generic like an objectthe compiler wouldn't be able to warn you. FWIW, the more verbose if(obj == null) ... else syntax does not require a cast, it may be a better fit in some cases.Rascality
T
9

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;
Timely answered 14/11, 2013 at 10:5 Comment(1)
Optionally, one one of the casts can be omitted. I'm not sure how this impacts readability.Kerrin
S
5

For the same reason that 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 Animals:

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.

Sire answered 14/11, 2013 at 10:18 Comment(0)
C
3

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

Chesser answered 14/11, 2013 at 10:8 Comment(5)
I suppose this is the bit I'm confused about. 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? Why is the explicit cast needed with the ?? operator and not needed when assigned directly like Animal a = c;Forgot
Imagine that the operator is a function that returns a value of the same type as a parameter or the default value like: T Func<T>(T a, T defaultValue)Verify
@Forgot With this info d ?? c; compiler have no clue about you need return type as Animal. So it checks whether it can covert dog to cat or cat to dog, If both not possible it failsChesser
@SriramSakthivel I think he gets how the compiler works. What he doesn't get is "why". The compiler could do this, but why doesn't it? That's his question.Sire
@Forgot You could as well have tried Console.WriteLine(d ?? c); The compiler has to figure out the type of the ?? expression based on the types of the operands d and c. It is not allowed to "look ahead" and say, hey, this guy is going to use the result later on for an Animal, I will use that type then.Maurili

© 2022 - 2024 — McMap. All rights reserved.