Ответ 1
Мы считаем, что этот пример обнаруживает ошибку в компиляторе С#.
Давайте делать то, что мы всегда должны делать, когда вы обнаруживаете ошибку компилятора: тщательно контрастировать с ожидаемым и наблюдаемым поведением.
Наблюдаемое поведение заключается в том, что программа производит 11 и 101 в качестве первого и второго выходов соответственно.
Каково ожидаемое поведение? Есть два "виртуальных слота". Первый вывод должен быть результатом вызова метода в слоте Foo(T)
. Второй вывод должен быть результатом вызова метода в слоте Foo(S)
.
Что происходит в этих слотах?
В экземпляре Base<T,S>
метод return 1
отправляется в слот Foo(T)
, а метод return 2
отправляется в слот Foo(S)
.
В случае Intermediate<T,S>
метод return 11
отправляется в слот Foo(T)
, а метод return 2
отправляется в слот Foo(S)
.
Надеюсь, до сих пор вы согласны со мной.
В случае Conflict
существует четыре возможности:
- Возможность: метод
return 11
отправляется в слотFoo(T)
, а методreturn 101
- в слотFoo(S)
. - Возможность двух: метод
return 101
находится в слотеFoo(T)
, а методreturn 2
- в слотеFoo(S)
. - Возможность три: метод
return 101
идет в обоих слотах. - Возможность четыре: компилятор обнаруживает, что программа неоднозначна и выдает ошибку.
Вы ожидаете, что здесь будет одна из двух вещей, основанная на разделе 10.6.4 спецификации. Или:
- Компилятор определит, что метод в
Conflict
переопределяет метод вIntermediate<string, string>
, потому что метод в промежуточном классе найден первым. В этом случае возможность двух - правильное поведение. Или: - Компилятор определит, что метод в
Conflict
неоднозначен в отношении того, какое оригинальное объявление оно переопределяет, и, следовательно, возможность четвертая является правильной.
В любом случае вероятность невозможна.
Это не 100% ясный, я признаю, что из этих двух правильных. Мое личное чувство заключается в том, что более разумное поведение заключается в том, чтобы рассматривать метод переопределения как частную реализацию промежуточного класса; вопрос, на мой взгляд, заключается не в том, переопределяет ли промежуточный класс метод базового класса, а в том, объявляет ли он метод с соответствующей сигнатурой. В этом случае правильное поведение будет заключаться в том, чтобы выбрать возможность четырех.
То, что делает настоящий компилятор, - это то, что вы ожидаете: он выбирает возможность два. Поскольку промежуточный класс имеет член, который соответствует, мы выбираем его как "вещь для переопределения", независимо от того, что метод не объявлен в промежуточном классе. Компилятор определяет, что Intermediate<string, string>.Foo
- это метод, переопределенный Conflict.Foo
, и соответственно испускает код. Он не вызывает ошибку, потому что он считает, что программа не ошибается.
Итак, если компилятор правильно анализирует код, выбирая возможность два и не создавая ошибку, то почему во время выполнения кажется, что компилятор выбрал возможность один, а не возможность два?
Поскольку создание программы, которая приводит к объединению двух методов в рамках общей конструкции, - это поведение, определяемое реализацией для среды выполнения. В этом случае среда исполнения может сделать что угодно! Он может выбрать ошибку типа загрузки. Это может привести к проверке. Он может разрешить программу, но заполнить слоты в соответствии с определенным критерием по своему выбору. И на самом деле последнее - это то, что он делает. Среда выполнения просматривает программу, испускаемую компилятором С#, и сама решает, что возможность - это правильный способ анализа этой программы.
Итак, теперь у нас есть довольно философский вопрос о том, является ли это ошибкой компилятора; компилятор следует разумной интерпретации спецификации, и все же мы по-прежнему не получаем ожидаемого поведения. В этом смысле это очень сложная ошибка компилятора. Задача компилятора - перевести программу, написанную на С#, в точно эквивалентную программу, написанную в IL. Компилятор не выполняет этого; это перевод программы, написанной на С#, в программу, написанную на IL, которая имеет поведение, определенное реализацией, а не поведение, указанное спецификацией языка С#.
Как четко описывает Сэм в своем блоге, мы хорошо знаем об этом несоответствии между типами топологий, которые язык С# наделяет конкретными значениями и какие топологии CLR наделяет конкретными значениями. Язык С# достаточно понятен, что вероятность двух, возможно, правильная, но нет кода, который мы можем испустить, что делает CLR, потому что CLR в принципе имеет поведение, определенное реализацией, в любое время, когда два метода объединяются, чтобы иметь одну и ту же подпись. Поэтому наш выбор:
- Ничего не делай. Позвольте этим сумасшедшим, нереалистичным программам продолжать поведение, которое точно не соответствует спецификации С#.
- Используйте эвристику. Как отмечает Сэм, мы можем быть более умны в использовании механизмов метаданных, чтобы сообщить CLR, какие методы переопределяют другие методы. Но... эти механизмы используют сигнатуры метода для устранения двусмысленных случаев, и теперь мы вернулись в ту же лодку, что и раньше; теперь мы используем механизм с поведением, определенным реализацией, чтобы устранить проблему с реализацией, определяемой реализацией! Это не стартер.
- Причина, по которой компилятор создает предупреждения или ошибки, когда он может испускать программу, поведение которой определяется реализацией среды выполнения.
- Исправить CLR, чтобы поведение топологий типов, которые вызывают методы для унификации в сигнатуре, хорошо определено и соответствует языку С#.
Последний выбор чрезвычайно дорог. Платя эту стоимость покупает у нас совершенно мало пользы для пользователей и напрямую берет бюджет от решения реалистичных проблем, с которыми сталкиваются пользователи, пишущие разумные программы. И в любом случае решение сделать это полностью из моих рук.
Поэтому мы в команде компилятора С# решили использовать комбинацию первой и третьей стратегий; иногда мы создаем предупреждения или ошибки для таких ситуаций, и иногда мы ничего не делаем и позволяем программе делать что-то странное во время выполнения.
Поскольку на практике подобные программы очень редко возникают в реалистичных сценариях бизнес-программирования, я не чувствую себя очень плохо в этих случаях. Если бы они были дешевыми и их легко исправить, мы бы их исправили, но они не были дешевыми и легкими для исправления.
Если этот вопрос вас интересует, см. мою статью о еще одном способе, в котором создание двух методов для объединения приводит к предупреждению и определению, определяемому реализацией:
http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx
http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/odious-ambiguous-overloads-part-two.aspx