У вас определенно есть серьезные недоразумения о том, как работает ковариация, но мне не на 100% ясно, что это такое. Позвольте мне сначала сказать, что такое интерфейсы, а затем мы можем построчно рассмотреть ваш вопрос и указать на все недоразумения.
Думайте об интерфейсе как о наборе «слотов», где каждый слот имеет контракт , а содержит метод, который выполняет этот контракт. Например, если у нас есть:
interface IFoo { Mammal X(Mammal y); }
тогда IFoo
имеет одну ячейку, и эта ячейка должна содержать метод, который берет млекопитающее и возвращает млекопитающее.
Когда мы неявным или явным образом преобразуем ссылку в тип интерфейса, мы никоим образом не меняем ссылку . Скорее, мы проверяем , что указанный тип уже имеет действительную таблицу слотов для этого интерфейса. Так что если у нас есть:
class C : IFoo {
public Mammal X(Mammal y)
{
Console.WriteLine(y.HairColor);
return new Giraffe();
}
}
и позже
C c = new C();
IFoo f = c;
Думайте о C как о наличии маленькой таблицы с надписью «если C преобразуется в IFoo, C.X попадает в слот IFoo.X»
Когда мы конвертируем c в f, c и f имеют точно такой же контент . Они одинаковые ссылки . Мы только что подтвердили , что тип c имеет таблицу слотов, совместимую с IFoo.
Теперь давайте пройдемся по вашему сообщению.
Invoking IExtract<object>.Extract()
вызывает IExtract<string>.Extract()
, что подтверждается выводом.
Давайте разберемся с этим.
У нас есть sampleClassOfString
, который реализует IExtract<string>
. Его тип имеет «таблицу слотов», которая гласит: «Мой Extract
входит в слот для IExtract<string>.Extract
».
Теперь, когда sampleClassOfString
конвертируется в IExtract<object>
, мы снова должны сделать проверку. Содержит ли тип sampleClassOfString
таблицу слотов интерфейса, подходящую для IExtract<object>
? Да, это так: мы можем использовать существующую таблицу для IExtract<string>
для этой цели .
Почему мы можем использовать его, даже если это два разных типа? Потому что все контракты все еще выполняются .
IExtract<object>.Extract
имеет контракт: это метод, который ничего не берет и возвращает object
. Ну, метод, который находится в слоте IExtract<string>.Extract
, соответствует этому контракту; он ничего не берет и возвращает строку, которая является объектом.
Поскольку все контракты выполнены, мы можем использовать таблицу слотов IExtract<string>
, которая у нас уже есть. Назначение выполнено успешно, и все вызовы пройдут через таблицу слотов IExtract<string>
.
IExtract<object>
НЕ находится в иерархии наследования, содержащей IExtract<string>
Правильно.
за исключением того факта, что C # сделал IExtract<string>
присваиваемым IExtract<object>
.
Не путайте эти две вещи; Они не то же самое. Наследование - это свойство, что член базового типа также является членом производного типа . Совместимость назначений - это свойство, которое экземпляру одного типа может быть присвоено переменной другого типа. Это логически очень разные!
Да, существует связь, поскольку деривация подразумевает как совместимость присваивания, так и наследование ; если D является производным типом базового типа B, то экземпляр D можно назначить переменной типа B, и все наследуемые члены B являются членами D.
Но не путайте эти две вещи; только то, что они связаны, не означает, что они одинаковы. Есть на самом деле языки, где они разные; то есть существуют языки, в которых наследование ортогонально совместимости присваивания. C # просто не один из них, и вы так привыкли к миру, где наследование и совместимость присваиваний так тесно связаны, что вы никогда не учились видеть их как отдельные. Начни думать о них как о разных вещах, потому что они есть.
Ковариация - это расширение отношения совместимости присваивания для типов, которые не находятся в иерархиях наследования. Вот что означает ковариация ; отношение совместимости присваивания равно ковариантному , если отношение сохраняется при сопоставлении с универсальным . «Яблоко можно использовать там, где нужен фрукт, поэтому последовательность яблок можно использовать там, где требуется фрукт» - ковариация . Отношение совместимости назначения сохраняется при отображении в последовательности .
Но IExtract<string>
просто НЕ имеет метода с именем Extract()
, который он наследует от IExtract<object>
Это верно. Не существует наследования между IExtract<string>
и IExtract<object>
. Однако между ними существует совместимость , потому что любой метод Extract
, который соответствует контракту IExtract<string>.Extract
, является также методом, который соответствует контракту IExtract<object>.Extract
. Поэтому таблица слотов первого может использоваться в ситуации, требующей последнего.
Разумно ли было бы сказать, что СОБСТВЕННЫЙ IExtract<string>
, по совпадению (или по замыслу) с аналогичным названием Extract()
метод скрывает IExtract<object>
метод Extract ()?
Абсолютно нет. Там нет никакого сокрытия вообще. «Скрытие» происходит, когда производный тип имеет элемент с тем же именем, что и унаследованный элемент базового типа, а новый элемент скрывает старый для поиска имен во время компиляции . Сокрытие является исключительно концепцией поиска имени во время компиляции; он не имеет никакого отношения к тому, как интерфейсы работают во время выполнения.
А что это за хак?
АБСОЛЮТНО НЕ .
Я пытаюсь не находить предложение оскорбительным, и в основном это удается. : -)
Эта функция была тщательно разработана экспертами; это звук (по модулю распространяющийся на существующие несоответствия в C #, такие как небезопасная ковариация массивов), и он был реализован с большой осторожностью и проверкой. В этом нет абсолютно ничего «хакерского».
так что же происходит, когда я вызываю IExtract<object>.Extract()
?
Логически, вот что происходит:
Когда вы преобразуете ссылку на класс в IExtract<object>
, мы проверяем, что в ссылке есть таблица слотов, совместимая с IExtract<object>
.
Когда вы вызываете Extract
, мы ищем содержимое слота Extract
в таблице слотов, которую мы определили как совместимую с IExtract<object>
. Поскольку это та же таблица слотов , что и у объекта, уже имеющегося для IExtract<string>
, происходит то же самое: метод класса Extract
находится в этом слоте, поэтому он вызывается.
На практике ситуация немного сложнее; в логике вызова есть куча механизмов, обеспечивающих хорошую производительность в обычных случаях. Но по логике вы должны думать об этом как о том, чтобы найти метод в таблице, а затем вызвать этот метод.
Делегаты также могут быть помечены как ковариантные и контравариантные. Как это работает?
Логически вы можете рассматривать делегатов как просто интерфейсы, которые имеют единственный метод, называемый Invoke, и это следует из этого. На практике, конечно, механизмы несколько отличаются благодаря таким вещам, как состав делегатов, но, возможно, теперь вы можете увидеть, как они могут работать.
Где я могу узнать больше?
Это что-то вроде пожарного шланга:
https://stackoverflow.com/search?q=user%3A88656+covariance
поэтому я бы начал сверху:
Разница между ковариацией и контрастностью
Если вы хотите историю функции в C # 4.0, начните здесь:
https://blogs.msdn.microsoft.com/ericlippert/2007/10/16/covariance-and-contravariance-in-c-part-one/
Обратите внимание, что это было написано до того, как мы остановились на "in" и "out" в качестве ключевых слов для контравариантности и ковариации.
Здесь можно найти множество других статей в хронологическом порядке "новейшей первой":
https://blogs.msdn.microsoft.com/ericlippert/tag/covariance-and-contravariance/
и несколько здесь:
https://ericlippert.com/category/covariance-and-contravariance/
УПРАЖНЕНИЕ: Теперь, когда вы примерно знаете, как это работает за кулисами, как вы думаете, что это делает?
interface IFrobber<out T> { T Frob(); }
class Animal { }
class Zebra: Animal { }
class Tiger: Animal { }
// Please never do this:
class Weird : IFrobber<Zebra>, IFrobber<Tiger>
{
Zebra IFrobber<Zebra>.Frob() => new Zebra();
Tiger IFrobber<Tiger>.Frob() => new Tiger();
}
…
IFrobber<Animal> weird = new Weird();
Console.WriteLine(weird.Frob());
? Подумайте об этом и посмотрите, сможете ли вы решить, что происходит.