Реализует ли я <D>I <B>, если I <D>конвертируется в I <B>путем преобразования отклонений? - PullRequest
20 голосов
/ 03 января 2012
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(); }
}

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

Ответы [ 4 ]

13 голосов
/ 03 января 2012

Это сложно.

Вызов 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 должно быть DC, но почему содержимое слота ICBC также должно быть DC? Есть ли в спецификации C # что-нибудь, оправдывающее такое поведение?

Наиболее близким является раздел 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> a повторной реализацией из ICloneable<Base>? Не совсем понятно, что так и должно быть. Повторная реализация интерфейса IFoo представляет собой повторную реализацию каждого базового интерфейса IFoo, но ICloneable<Base> не является базовым интерфейсом из ICloneable<Derived>!

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

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

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

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

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

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

Итак, подведем итог:

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

Еще один пример того, как вариант унификации интерфейса демонстрирует необоснованное, зависящее от реализации поведение в реализации CLR «первая подгонка», см.http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx

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

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

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

4 голосов
/ 03 января 2012

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

0 голосов
/ 03 января 2012

Мне кажется, что соответствующая часть спецификации будет той, которая контролирует, какое из двух возможных неявных эталонных преобразований используется для присвоения 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>.)

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

0 голосов
/ 03 января 2012

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

ICloneable<Base> cb = d;

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

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