Подтвердить что ты не робот

Предупреждение или ошибка (или отказ во время выполнения), когда контравариантность приводит к двусмысленности

Во-первых, помните, что .NET String - это IConvertible и ICloneable.

Теперь рассмотрим следующий довольно простой код:

//contravariance "in"
interface ICanEat<in T> where T : class
{
  void Eat(T food);
}

class HungryWolf : ICanEat<ICloneable>, ICanEat<IConvertible>
{
  public void Eat(IConvertible convertibleFood)
  {
    Console.WriteLine("This wolf ate your CONVERTIBLE object!");
  }

  public void Eat(ICloneable cloneableFood)
  {
    Console.WriteLine("This wolf ate your CLONEABLE object!");
  }
}

Затем попробуйте следующее (внутри некоторого метода):

ICanEat<string> wolf = new HungryWolf();
wolf.Eat("sheep");

Когда кто-то компилирует это, вы не получаете никакой ошибки компилятора или предупреждения. При запуске он выглядит так, как вызванный метод зависит от порядка списка интерфейсов в моем объявлении class для HungryWolf. (Попробуйте поменять местами два интерфейса в разделенном запятой (,).)

Вопрос прост: Должно ли это давать предупреждение во время компиляции (или бросать во время выполнения)?

Я, вероятно, не первый, кто придумал такой код. Я использовал контравариантность интерфейса, но вы можете сделать совершенно аналогичный пример с covarainace интерфейса. И фактически Мистер Липперт сделал именно это давным-давно. В комментариях в своем блоге почти все согласны с тем, что это должна быть ошибка. Но они позволяют это молча. Почему?

---

Расширенный вопрос:

Выше мы использовали, что String - это IConvertible (интерфейс) и ICloneable (интерфейс). Ни один из этих двух интерфейсов не вытекает из другого.

Теперь вот пример с базовыми классами, который в некотором смысле немного хуже.

Помните, что StackOverflowException - это SystemException (прямой базовый класс) и Exception (базовый класс базового класса). Тогда (если ICanEat<> как раньше):

class Wolf2 : ICanEat<Exception>, ICanEat<SystemException>  // also try reversing the interface order here
{
  public void Eat(SystemException systemExceptionFood)
  {
    Console.WriteLine("This wolf ate your SYSTEM EXCEPTION object!");
  }

  public void Eat(Exception exceptionFood)
  {
    Console.WriteLine("This wolf ate your EXCEPTION object!");
  }
}

Протестируйте его с помощью:

static void Main()
{
  var w2 = new Wolf2();
  w2.Eat(new StackOverflowException());          // OK, one overload is more "specific" than the other

  ICanEat<StackOverflowException> w2Soe = w2;    // Contravariance
  w2Soe.Eat(new StackOverflowException());       // Depends on interface order in Wolf2
}

По-прежнему нет предупреждений, ошибок или исключений. Все еще зависит от порядка списка интерфейсов в объявлении class. Но причина, по которой мне кажется хуже, заключается в том, что на этот раз кто-то может подумать, что разрешение перегрузки всегда будет выбирать SystemException, потому что это более специфично, чем просто Exception.


Статус до открытия щели: три ответа от двух пользователей.

Статус в последний день щедрости: по-прежнему новых ответов не получено. Если ответы не появятся, я должен наградить премию Муслиму Бен Дхау.

4b9b3361

Ответ 1

Я считаю, что компилятор делает лучшее в VB.NET с предупреждением, но я все еще не думаю, что это происходит достаточно далеко. К сожалению, "правильная вещь", вероятно, либо требует отклонения того, что потенциально полезно (реализация одного и того же интерфейса с двумя ковариантными или контравариантными параметрами типового типа) или введение чего-то нового в язык.

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

Я знаю, как есть ICloneable, или что-либо, реализующее или наследующее от него, определенным образом.

И я также знаю, как есть IConvertible, или что-либо, реализующее или наследующее от него, определенным образом.

Однако он никогда не заявляет, что он должен делать, если он получает на своей пластине что-то, что как ICloneable, так и IConvertible. Это не вызывает у компилятора горе, если ему задан экземпляр HungryWolf, поскольку он может с уверенностью сказать: "Эй, я не знаю, что здесь делать!". Но это даст компилятор горе, когда ему будет предоставлен экземпляр ICanEat<string>. Компилятор понятия не имеет, каков фактический тип объекта в переменной, только то, что он определенно реализует ICanEat<string>.

К сожалению, когда a HungryWolf хранится в этой переменной, он неоднозначно реализует этот точный интерфейс дважды. Поэтому, конечно, мы не можем выпустить ошибку, пытающуюся вызвать ICanEat<string>.Eat(string), поскольку этот метод существует и был бы вполне применим для многих других объектов, которые могли бы быть помещены в переменную ICanEat<string> (batwad уже упоминал об этом в одном из своих ответов).

Кроме того, хотя компилятор может жаловаться на то, что назначение объекта HungryWolf переменной ICanEat<string> неоднозначно, это не может помешать ему в два этапа. A HungryWolf может быть назначено переменной ICanEat<IConvertible>, которая может быть передана другим методам и в конечном итоге назначена переменной ICanEat<string>. Оба из них являются совершенно законными назначениями, и компилятор не сможет жаловаться ни на один.

Таким образом, вариант должен запретить классу HungryWolf реализовать как ICanEat<IConvertible>, так и ICanEat<ICloneable>, когда параметр ICanEat generic type является контравариантным, поскольку эти два интерфейса могут объединяться. Однако этот удаляет возможность кодировать что-то полезное без альтернативного метода обхода.

Вариант 2, к сожалению, будет требовать изменения компилятора, как IL, так и CLR. Это позволило бы классу HungryWolf реализовать оба интерфейса, но для этого также потребовалась бы реализация интерфейса ICanEat<IConvertible & ICloneable>, где параметр generic type реализует оба интерфейса. Вероятно, это не лучший синтаксис (как выглядит подпись этого метода Eat(T), Eat(IConvertible & ICloneable food)?). Вероятно, лучшим решением будет создание автоматически генерируемого родового типа на классе реализации, чтобы определение класса было примерно таким:

class HungryWolf:
    ICanEat<ICloneable>, 
    ICanEat<IConvertible>, 
    ICanEat<TGenerated_ICloneable_IConvertible>
        where TGenerated_ICloneable_IConvertible: IConvertible, ICloneable {
    // implementation
}

Затем IL должен был измениться, чтобы позволить создавать типы реализации интерфейса, как и общие классы для инструкции callvirt:

.class auto ansi nested private beforefieldinit HungryWolf 
    extends 
        [mscorlib]System.Object
    implements 
        class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.ICloneable>,
        class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.IConvertible>,
        class NamespaceOfApp.Program/ICanEat`1<class ([mscorlib]System.IConvertible, [mscorlib]System.ICloneable>)!TGenerated_ICloneable_IConvertible>

Затем CLR должен будет обработать инструкции callvirt, построив реализацию интерфейса для HungryWolf с string как параметр типового типа для TGenerated_ICloneable_IConvertible и проверив, будет ли он соответствовать лучше других реализаций интерфейса.

Для ковариации все это было бы проще, так как дополнительные интерфейсы, необходимые для реализации, не должны были быть типичными параметрами типа с ограничениями , а просто самым производным базовым типом между двумя другими типами, который известен во время компиляции.

Если один и тот же интерфейс реализован более двух раз, то количество дополнительных интерфейсов, требуемых для реализации, растет экспоненциально, но это будет стоить гибкости и безопасности типов при реализации нескольких контравариантных (или ковариантных) на одном класс.

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


изменить
Спасибо Jeppeнапомнив мне, что ковариация не проще, чем контравариантность, из-за того, что необходимо учитывать и общие интерфейсы. В случае string и char[] множество наибольших общности было бы {object, ICloneable, IEnumerable<char>} (IEnumerable покрыто IEnumerable<char>).

Однако для этого потребуется новый синтаксис ограничения параметра универсального типа интерфейса, чтобы указать, что для параметра общего типа требуется

  • наследует от указанного класса или класса, реализующего хотя бы один из указанных интерфейсов
  • реализовать хотя бы один из указанных интерфейсов

Возможно, что-то вроде:

interface ICanReturn<out T> where T: class {
}

class ReturnStringsOrCharArrays: 
    ICanReturn<string>, 
    ICanReturn<char[]>, 
    ICanReturn<TGenerated_String_ArrayOfChar>
        where TGenerated_String_ArrayOfChar: object|ICloneable|IEnumerable<char> {
}

Общий параметр типа TGenerated_String_ArrayOfChar в этом случае (где один или несколько интерфейсов являются общими) всегда должен рассматриваться как object, хотя общий базовый класс уже получен из object; потому что общий тип может реализовать общий интерфейс, не наследуя от общего базового класса.

Ответ 2

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

По какой-то причине компилятор VB.Net выдает предупреждение в таком случае, подобное

Интерфейс "ICustom (Of Foo)" неоднозначен с другим реализованным интерфейсом "ICustom (Of Boo)" из-за параметров "In" и "Out" в "Интерфейсе ICustom (Of In T)"

Я согласен с тем, что компилятор С# тоже должен отправить подобное предупреждение. Проверьте этот вопрос о переполнении стека. Г-н Липперт подтвердил, что время выполнения выбирает одно и что такого рода программирование следует избегать.

Ответ 3

Здесь одна попытка оправдания отсутствия предупреждения или ошибки, с которой я только что пришла, и я даже не пил!

Расскажите о своем коде дальше, каково намерение?

ICanEat<string> beast = SomeFactory.CreateRavenousBeast();
beast.Eat("sheep");

Ты что-то кормишь. Как зверь на самом деле съедает его до зверя. Если бы ему дали суп, он мог бы решить использовать ложку. Он может использовать нож и вилку, чтобы съесть овец. Это может разорвать его на клочья зубами. Но дело в том, что мы вызываем этого класса, нам все равно, как он ест, мы просто хотим, чтобы его кормили.

В конечном счете, до зверя решать, как есть то, что ему дано. Если зверь решает, что он может съесть ICloneable и IConvertible, то это до зверя, чтобы выяснить, как обращаться с обоими случаями. Зачем охотнику следить за тем, как животные едят свои блюда?

Если это вызвало ошибку времени компиляции, вы делаете внедрение нового интерфейса нарушением. Например, если вы отправляете HungryWolf как ICanEat<ICloneable>, а затем через несколько месяцев добавьте ICanEat<IConvertible>, вы разрываете все места, где оно было использовано с типом, который реализует оба интерфейса.

И он режет оба пути. Если общий аргумент реализует только ICloneable, он будет работать довольно счастливо с волком. Испытания проходят, грузите это, большое спасибо мистеру Вольфу. В следующем году вы добавите поддержку IConvertible к вашему типу и BANG! На самом деле это не очень полезно.

Ответ 4

Другое объяснение: нет никакой двусмысленности, поэтому предупреждение о компиляторе отсутствует.

Потерпи меня.

ICanEat<string> wolf = new HungryWolf();
wolf.Eat("sheep");

Ошибка отсутствует, поскольку ICanEat<T> содержит только один метод с именем Eat с этими параметрами. Но скажите это вместо этого, и вы получите сообщение об ошибке:

HungryWolf wolf = new HungryWolf();
wolf.Eat("sheep");

HungryWolf является неоднозначным, а не ICanEat<T>. В ожидании ошибки компилятора вы попросите компилятор посмотреть значение переменной в точке, в которой Eat вызывается и работает, если он неоднозначен, что не обязательно то, что он может сделать. Рассмотрим класс UnambiguousCow, который реализует только ICanEat<string>.

ICanEat<string> beast;

if (somethingUnpredictable)
    beast = new HungryWolf();
else
    beast = new UnambiguousCow();

beast.Eat("dinner");

Где и как вы ожидаете, что возникнет ошибка компилятора?