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

Ковариация параметра общего типа и реализация нескольких интерфейсов

Если у меня есть общий интерфейс с параметром ковариантного типа, например:

interface IGeneric<out T>
{
    string GetName();
}

И если я определяю эту иерархию классов:

class Base {}
class Derived1 : Base{}
class Derived2 : Base{}

Затем я могу реализовать интерфейс дважды в одном классе, например, используя явную реализацию интерфейса:

class DoubleDown: IGeneric<Derived1>, IGeneric<Derived2>
{
   string IGeneric<Derived1>.GetName()
   {
     return "Derived1";
   }

   string IGeneric<Derived2>.GetName()
   {
     return "Derived2";
   }  
}

Если я использую класс (non-generic) DoubleDown и передаю его в IGeneric<Derived1> или IGeneric<Derived2>, он будет функционировать как ожидалось:

var x = new DoubleDown();
IGeneric<Derived1> id1 = x;        //cast to IGeneric<Derived1>
Console.WriteLine(id1.GetName());  //Derived1
IGeneric<Derived2> id2 = x;        //cast to IGeneric<Derived2>
Console.WriteLine(id2.GetName());  //Derived2

Однако при нажатии x на IGeneric<Base> получается следующий результат:

IGeneric<Base> b = x;
Console.WriteLine(b.GetName());   //Derived1

Я ожидал, что компилятор выдает ошибку, поскольку вызов является неоднозначным между двумя реализациями, но он возвратил первый объявленный интерфейс.

Почему это разрешено?

(вдохновленный класс, реализующий два разных IObservables?. Я пытался показать коллеге, что это провалится, но почему-то это не так)

4b9b3361

Ответ 1

Компилятор не может выдать ошибку в строке

IGeneric<Base> b = x;
Console.WriteLine(b.GetName());   //Derived1

потому что нет никакой двусмысленности, о которой может знать компилятор. GetName() фактически является допустимым методом на интерфейсе IGeneric<Base>. Компилятор не отслеживает время выполнения b, чтобы знать, что есть тип, который может вызвать двусмысленность. Поэтому он остался до времени выполнения, чтобы решить, что делать. Время выполнения может вызвать исключение, но разработчики CLR, по-видимому, решили против этого (что я лично считаю хорошим решением).

Другими словами, скажем, что вместо этого вы просто написали метод:

public void CallIt(IGeneric<Base> b)
{
    string name = b.GetName();
}

и вы не предоставляете классы, реализующие IGeneric<T> в вашей сборке. Вы распространяете это, и многие другие реализуют этот интерфейс только один раз и могут просто вызвать ваш метод. Однако кто-то в конечном итоге потребляет вашу сборку и создает класс DoubleDown и передает его в ваш метод. В какой момент компилятор должен выдать ошибку? Разумеется, уже скомпилированная и распределенная сборка, содержащая вызов GetName(), не может привести к ошибке компилятора. Можно сказать, что назначение от DoubleDown до IGeneric<Base> вызывает неоднозначность. но еще раз мы могли бы добавить еще один уровень косвенности в исходную сборку:

public void CallItOnDerived1(IGeneric<Derived1> b)
{
    return CallIt(b); //b will be cast to IGeneric<Base>
}

И снова многие потребители могут называть либо CallIt, либо CallItOnDerived1 и быть в порядке. Но наш потребительский переход DoubleDown также делает совершенно юридический вызов, который не мог вызвать ошибку компилятора при вызове CallItOnDerived1, поскольку преобразование из DoubleDown в IGeneric<Derived1> должно быть, конечно, в порядке. Таким образом, нет никакой точки, в которой компилятор может выдать ошибку, отличную от определения DoubleDown, но это исключило бы возможность делать что-то потенциально полезное без обходного пути.

Я действительно более подробно ответил на этот вопрос в другом месте, а также предоставил потенциальное решение, если язык можно было изменить:

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

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

Ответ 2

Если вы протестировали оба из:

class DoubleDown: IGeneric<Derived1>, IGeneric<Derived2> {
    string IGeneric<Derived1>.GetName() {
        return "Derived1";
    }

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

class DoubleDown: IGeneric<Derived2>, IGeneric<Derived1> {
    string IGeneric<Derived1>.GetName() {
        return "Derived1";
    }

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

Вы, должно быть, уже поняли, что результат в реальности, изменяется с порядком, в котором объявляются интерфейсы для реализации. Но я бы сказал, что он просто не указан.

Во-первых, спецификация (§13.4.4. Отображение интерфейса) говорит:

  • Если встречается более одного члена, это неуказано, членом которого является реализация I.M.
  • Эта ситуация может иметь место только, если S - это построенный тип, где два члена, объявленные в родовом типе, имеют разные сигнатуры, но аргументы типа делают их подписи идентичными.

Здесь у нас есть два вопроса:

  • Q1: У ваших общих интерфейсов есть разные подписи?
    А1: Да. Они IGeneric<Derived2> и IGeneric<Derived1>.

  • Q2: Может ли оператор IGeneric<Base> b=x; сделать свои подписи идентичными с аргументами типа?
    A2: Нет. Вы вызывали метод с помощью общего определения ковариантного интерфейса.

Таким образом, ваш вызов соответствует неуказанному условию. Но как это могло произойти?
Помните, независимо от того, какой интерфейс вы указали для ссылки на объект типа DoubleDown, он всегда является DoubleDown. То есть, он всегда имеет два метода GetName. Фактически, интерфейс, который вы указываете для его ссылки, выполняет выбор контракта.

Ниже приведена часть захваченного изображения из реального теста

enter image description here

Это изображение показывает, что будет возвращено с GetMembers во время выполнения. Во всех случаях вы ссылаетесь на него, IGeneric<Derived1>, IGeneric<Derived2> или IGeneric<Base>, ничего другого. После двух изображений, показанных более подробно:

enter image description hereenter image description here

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

И теперь вы просто знаете, почему.

Ответ 3

Вопрос: "Почему это не приводит к предупреждению компилятора?". В VB он делает (я его реализовал).

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

  • В VB, если вы объявляете класс C, который реализует как IEnumerable(Of Fish), так и IEnumerable(Of Dog), тогда он дает предупреждение о том, что оба конфликтуют в общем случае IEnumerable(Of Animal). Этого достаточно, чтобы искоренить дисперсию - двусмысленность от кода, полностью написанного в VB.

    Однако это не помогает, если класс проблемы был объявлен в С#. Также обратите внимание, что вполне разумно объявить такой класс, если никто не вызывает на нем проблемный элемент.

  • В VB, если вы выполняете кастинг из такого класса C в IEnumerable(Of Animal), тогда он дает предупреждение о трансляции. Этого достаточно, чтобы исключить дисперсию-неоднозначность , даже если вы импортировали класс проблемы из метаданных.

    Однако это плохое место предупреждения, потому что оно не действует: вы не можете пойти и изменить состав. Единственное действенное предупреждение для людей было бы , чтобы вернуться и изменить определение класса. Также обратите внимание, что полностью выполнять <личный > <личный > , если никто не вызывает на нем проблемный элемент.

  • Вопрос:

    Почему VB испускает эти предупреждения, но С# нет?

    Ответ:

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

    Эрик Липперт делал их на С#. У него была мудрость и зрелость, чтобы понять, что кодирование таких предупреждений в компиляторе потребует много времени, которое можно было бы лучше потратить в другом месте, и было достаточно сложным, чтобы оно несло высокого риска. На самом деле у компиляторов VB были ошибки в этих самых предупреждениях, которые были зафиксированы только в VS2012.

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

  • Вопрос:

    Как CLR разрешает двусмысленность при выборе того, для какого вызова?

    Ответ:

    Он основывает его на лексическом упорядочении операторов наследования в исходном исходном коде, то есть лексическом порядке, в котором вы объявили, что C реализует IEnumerable(Of Fish) и IEnumerable(Of Dog).

Ответ 4

Святая благость, много действительно хороших ответов здесь на то, что довольно сложный вопрос. Подведение итогов:

  • В спецификации языка четко не сказано, что делать здесь.
  • Этот сценарий обычно возникает, когда кто-то пытается эмулировать ковариацию интерфейса или контравариантность; теперь, когда С# имеет дисперсию интерфейса, мы надеемся, что меньше людей будут использовать этот шаблон.
  • Большую часть времени "просто выберите один" - разумное поведение.
  • Как CLR на самом деле выбирает, какая реализация используется в двусмысленном ковариантном преобразовании, определяется реализацией. В основном, он сканирует таблицы метаданных и выбирает первое совпадение, а С# генерирует таблицы в порядке исходного кода. Однако вы не можете полагаться на это поведение; либо может измениться без предупреждения.

Я бы добавил только одну вещь, а именно: плохая новость заключается в том, что семантика переопределения интерфейса точно не соответствует поведению, указанному в спецификации CLI, в сценариях, где возникают подобные разногласия. Хорошей новостью является то, что фактическое поведение CLR при повторной реализации интерфейса с такой двусмысленностью обычно является поведением, которое вы хотите. Обнаружение этого факта привело к энергичным дебатам между мной, Андерсом и некоторыми специалистами по поддержке CLI, и конечный результат не изменился ни в спецификации, ни в реализации. Поскольку большинство пользователей С# даже не знают, для чего нужно переиздавать интерфейс, мы надеемся, что это не будет отрицательно влиять на пользователей. (Ни один клиент никогда не привлекал это к моему вниманию.)

Ответ 5

Попытка вникать в "спецификации языка С#", похоже, что поведение не указано (если я не заблудился на моем пути).

7.4.4 Вызов функции-члена

Обработка времени выполнения вызова функции-члена состоит из следующих шагов, где M является членом функции, и, если M является членом-экземпляром, E является выражением экземпляра:

[...]

o Реализация члена функции для вызова определяется:

     
  

• Если тип времени компиляции E является интерфейсом, то элемент функции для вызова - это реализация M, предоставляемая типом времени выполнения экземпляра, на который ссылается E. Этот член функции определяется применением правила сопоставления интерфейса (§13.4.4), чтобы определить реализацию M, предоставленную типом времени выполнения экземпляра, на который ссылается E.

  

13.4.4 Отображение интерфейса

Отображение интерфейса для класса или структуры C находит реализацию для каждого члена каждого интерфейса, указанного в списке базового класса C. Реализация конкретного члена интерфейса IM, где я является интерфейсом, в котором объявлен член M, определяется путем изучения каждого класса или структуры S, начиная с C и повторяя для каждого последующего базового класса C, пока не будет найдено совпадение:

• Если S содержит объявление явной реализации члена интерфейса, которая соответствует я и M, то этот член является реализацией I.M.

• В противном случае, если S содержит объявление нестатического открытого члена, который соответствует M, то этот член является реализацией IM. Если встречается более одного члена, , он не указан, членом которого является реализация IM. Эта ситуация может возникнуть только в том случае, если S является построенным типом, в котором два члена, объявленные в родовом типе, имеют разные подписи, но аргументы типа делают их подписи идентичными.