Ответ 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", см.:
И для примера, в котором невариантная универсальная унификация методов интерфейса предоставляет необоснованное, зависящее от реализации поведение в реализации CLR "first fit", см.:
http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx
В этом случае вы действительно можете вызвать изменение в поведении программы, переупорядочивая текст программы, что действительно странно на С#.