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

Я <D> повторно реализую я <B>, если я <D> конвертируется в я <B> путем преобразования дисперсии?

interface ICloneable<out T>
{
    T Clone();
}

class Base : ICloneable<Base>
{
    public Base Clone() { return new Base(); }
}

class Derived : Base, ICloneable<Derived>
{
    new public Derived Clone() { return new Derived(); }
}

Учитывая эти объявления типов, какая часть спецификации С# объясняет, почему последняя строка следующего фрагмента кода печатает "True"? Могут ли разработчики полагаться на это поведение?

Derived d = new Derived();
Base b = d;
ICloneable<Base> cb = d;
Console.WriteLine(b.Clone() is Derived); // "False": Base.Clone() is called
Console.WriteLine(cb.Clone() is Derived); // "True": Derived.Clone() is called

Обратите внимание, что если параметр T type в ICloneable не был объявлен out, то обе строки будут печатать "False".

4b9b3361

Ответ 1

Это сложно.

Вызов b.Clone явно должен вызвать BC. Здесь нет никакого интерфейса! Метод вызова определяется полностью анализом времени компиляции. Поэтому он должен вернуть экземпляр Base. Это не очень интересно.

Вызов cb.Clone по контрасту чрезвычайно интересен.

Есть две вещи, которые мы должны установить, чтобы объяснить поведение. Во-первых: какой "слот" вызывается? Второй: какой метод находится в этом слоте?

У экземпляра Derived должно быть два слота, потому что есть два метода, которые должны быть реализованы: ICloneable<Derived>.Clone и ICloneable<Base>.Clone. Позвольте назвать эти слоты ICDC и ICBC.

Очевидно, что слот, который вызывается cb.Clone, должен быть слотом ICBC; нет никакой причины, чтобы компилятор знал, что слот ICDC существует даже на cb, который имеет тип ICloneable<Base>.

Какой метод входит в этот слот? Существует два метода: Base.Clone и Derived.Clone. Позвольте называть те BC и DC. Как вы обнаружили, содержимое этого слота на экземпляре Derived является DC.

Это кажется странным. Очевидно, что содержимое слота ICDC должно быть постоянным, но почему содержимое слота ICBC также должно быть DC? Есть ли что-либо в спецификации С#, которая оправдывает это поведение?

Ближайшим, который мы получаем, является раздел 13.4.6, посвященный "повторной реализации интерфейса". Вкратце, когда вы говорите:

class B : IFoo 
{
    ...
}
class D : B, IFoo
{
    ...
}

то, что касается методов IFoo, мы начинаем с нуля в D. Все, что B может сказать о том, какие методы отображения B относятся к методам IFoo, отбрасывается; D может выбирать те же сопоставления, что и B, или он может выбирать совершенно разные. Такое поведение может привести к некоторым непредвиденным ситуациям; вы можете прочитать о них подробнее здесь:

http://blogs.msdn.com/b/ericlippert/archive/2011/12/08/so-many-interfaces-part-two.aspx

Но: является ли реализация ICloneable<Derived> повторной реализацией ICloneable<Base>? Не совсем ясно, что это должно быть. Повторная реализация интерфейса IFoo является повторной реализацией каждого базового интерфейса IFoo, но ICloneable<Base> не является базовым интерфейсом ICloneable<Derived>!

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

Итак, что здесь происходит?

Что здесь происходит, так это время выполнения, необходимое для заполнения слота ICBC. (Как мы уже говорили, слот ICDC явно должен получить метод DC.) Среда выполнения думает, что это повторная реализация интерфейса, поэтому она делает это путем поиска из Derived to Base и выполняет совпадение первого соответствия. DC - это результат благодаря дисперсии, поэтому он выигрывает по BC.

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

Однако рассмотрим точный случай, который вы здесь описываете. Разумно предположить, что тот, кто называет ICloneable<Base>.Clone() на экземпляре Derived, хочет получить Derived обратно!

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

Итак, суммируя:

  • De facto, CLR выполняет поиск соответствия первого соответствия из производного на базу, как если бы это была повторная реализация интерфейса.
  • De jure, что не оправдано ни спецификацией С#, ни спецификацией CLI.
  • Мы не можем изменить реализацию, не нарушая людей.
  • Реализация интерфейсов, которые унифицируют конверсии с ошибками, опасна и запутанна; старайтесь избегать этого.

Еще один пример того, как унифицированная унификация интерфейса предоставляет необоснованное, зависящее от реализации поведение в реализации CLR "first fit", см.:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx

И для примера, в котором невариантная универсальная унификация методов интерфейса предоставляет необоснованное, зависящее от реализации поведение в реализации CLR "first fit", см.:

http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx

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

Ответ 2

Он может иметь только одно значение: метод new public Derived Clone() реализует как ICloneable<Base>, так и ICloneable<Derived>. Только явный вызов Base.Clone() вызывает скрытый метод.

Ответ 3

Я думаю, это потому, что вызов:

ICloneable<Base> cb = d;

без дисперсии, то cb может представлять только ICloneable<Base>. Но с дисперсией он также может представлять ICloneable<Derived>, который, очевидно, ближе и лучше отличается от d, чем отбрасывание на ICloneable<Base>.

Ответ 4

Мне кажется, что соответствующая часть спецификации будет той, которая контролирует, какая из двух возможных неявных ссылочных преобразований находится в игре для назначения ICloneable<Base> cb = d;. Два варианта, взятые из раздела 6.1.6, "неявные ссылочные преобразования":

  • От любого класса S до любого типа интерфейса T, если S реализует T.

(здесь Derived реализует ICloneable<Base>, согласно разделу 13.4, потому что "когда класс C непосредственно реализует интерфейс, все классы, производные от C, также реализуют интерфейс неявно", а Base непосредственно реализует ICloneable<Base>, поэтому Derived реализует его неявно.)

  • От любого ссылочного типа к интерфейсу или типу делегата T, если он имеет неявное идентификационное или ссылочное преобразование в интерфейс или тип делегата T0 и T0, является конвертируемым с вариацией (§13.1.3.2) к T.

(Здесь Derived неявно конвертируется в ICloneable<Derived>, потому что он реализует его напрямую, а ICloneable<Derived> имеет отклонение от вариации до ICloneable<Base>.)

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