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");
}
}
Null-coalescing Operator Use this operator to fall back on a given value. In cases where a statement could return null, the null-coalescing operator can be used to ensure a reasonable value gets returned. This code returns the name of an item or the default name if the item is null.
The null-coalescing operator ?? returns the value of its left-hand operand if it isn't null ; otherwise, it evaluates the right-hand operand and returns its result.
Nullable types work as a connector between a database and C# code to provide a way to transform the nulls to be used in C# code. Null Coalescing operators simplify the way to check for nulls and shorten the C# code when the developer is dealing with nulls.
The nullish coalescing operator ( ?? ) is a logical operator that returns its right-hand side operand when its left-hand side operand is null or undefined , and otherwise returns its left-hand side operand.
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;
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With