Нуль-коалесцирующая проблема с абстрактными базовыми/производными классами

Почему оператор С# null коалесценции не в состоянии понять это?

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

  Animal a = d ?? c;

Это даст ошибку

Оператор? не может применяться к операндам типа Dog и Cat

Кажется странным, учитывая следующие компиляции.

Animal a = d;
a = c;

Контекстный код:

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");
  }
}

Ответ 1

Одним из тонких правил проектирования С# является то, что С# никогда не вводит тип, который не был в выражении для начала. Поскольку Animal не находится в выражении d ?? c, тип Animal не является выбором.

Этот принцип применяется везде, где С# записывает типы. Например:

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

Компилятор не говорит "это должен быть массив животных", он говорит: "Я думаю, что вы допустили ошибку".

Это конкретная версия более общего правила проектирования, которое "дает ошибку, когда программа выглядит неоднозначно, а не делает предположение, что это может быть неправильно".

Другое конструкторское правило, которое вступает в игру здесь: причина о типах изнутри вовне, а не извне внутрь. То есть вы должны иметь возможность выработать тип всего выражения, глядя на его части, не глядя на его контекст. В вашем примере Animal происходит вне выражения ??; мы должны уметь выяснить, что такое тип выражения ??, а затем задать вопрос: "Этот тип совместим с контекстом?" вместо того, чтобы идти другим путем и говорить "здесь контекст - теперь выработайте тип выражения ??".

Это правило оправдано, потому что очень часто контекст неясен. В вашем случае контекст очень ясен; вещь присваивается Animal. Но как насчет:

var x = a ?? b;

Теперь выводится тип x. Мы не знаем тип контекста, потому что это то, что мы разрабатываем. Или

M(a ?? b)

Может быть две дюжины перегрузок M, и нам нужно знать, какой из них выбрать, исходя из типа аргумента. Очень трудно рассуждать по-другому и сказать, что "контекст может быть одним из этих дюжины вещей, оценивайте a??b в каждом контексте и разрабатывайте его тип".

Это правило нарушено для лямбда, которые анализируются на основе их контекста. Правильный и эффективный код был очень сложным; это заняло у меня большую часть года работы. Команда компилятора может делать больше функций, быстрее и лучше, не принимая на себя такие расходы, когда она не нужна.

Ответ 2

Прежде чем назначить его Animal a, c и d будут по-прежнему Cat и Dog соответственно. Следующее работает так, как вы ожидаете:

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

Ответ 3

По той же причине, что Animal a = (true)? d : c; не будет работать (используя тернарный оператор).

В соответствии со спецификацией С# тип выражения выводится следующим образом (цитирование Eric Lippert):

Второй и третий операнды оператора?: управляют типом условное выражение. Пусть X и Y - типы второго и третьих операндов. Тогда,

  • Если X и Y являются одним и тем же типом, то это тип условного выражения.
  • В противном случае, если неявное преобразование существует от X до Y, но не от Y до X, то Y является типом условного выражения.
  • В противном случае, если неявное преобразование существует от Y до X, но не от X до Y, то X является типом условного выражения.
  • В противном случае тип выражения не может быть определен, и возникает ошибка времени компиляции.

Так как никакого неявного преобразования существует от Dog to Cat, ни от Cat до Dog, то тип не может быть выведен. Тот же принцип применяется к нулевому коалесцирующему оператору.

Edit

Почему нулевой коалиция заботится о взаимоотношениях между Кошкой и Собакой, а не только о взаимоотношениях между Кошкой и Животным и Собакой и Животным?

Что касается того, почему компилятор не просто понимает, что оба оператора Animal s:

Это слишком большая банка червей. Нам нравится принцип, что тип выражения должно быть типом чего-то в выражении.

Ответ 4

Он терпит неудачу, потому что c не может быть преобразован в d неявно и наоборот. Obvoiously Cat не является Dog, а Dog не является Cat.

Попробуйте это

Animal a = (Animal)d ?? c;

Теперь мы говорим, что компилятор, что левый операнд стороны ?? равен Animal, и да, он может преобразовать "собака в животное", а также правый и боковой операнд ?? is Cat, который также может быть преобразован в "Животное". Теперь компилятор счастлив:)