Ограничения не являются частью сигнатуры метода
Что произойдет в этом случае?
class Animal { }
class Mammal : Animal { }
class Giraffe : Mammal { }
class Reptile : Animal { }
…
static void Foo<T>(T t) where T : Reptile { }
static void Foo(Animal animal) { }
static void Main()
{
Foo(new Giraffe());
}
Большинство людей предполагает, что при разрешении перегрузки (overload resolution) будет выбран второй перегруженный метод. На самом деле, при компиляции этой программы вы получите ошибку, в которой будет говориться, что T не может быть типом Giraffe. Является ли это ошибкой компилятора?
Нет, это поведение является корректным с точки зрения спецификации языка. Сначала мы пытаемся определить набор методов кандидатов. Совершенно очевидно, что второй перегруженный метод является членом этого набора. В случае успешного вывода типов (type inference), первый метод также будет членом этого набора.
Алгоритм выведения типов аргумента метода рассматривает только лишь возможность непротиворечивого выведения типа аргумента метода по типам параметров. Алгоритм выведения типа аргумента совершенно не касается вопроса о том, является ли результирующий метод не подходящим в каком-то другом аспекте. Его единственная задача – найти лучший возможный тип аргументов метода по предоставленным аргументам. Очевидно, что в данном случае лучшим типом для T является Giraffe, поэтому будет выведен именно этот тип.
Итак, у нас есть два метода в списке кандидатов, Foo<Giraffe> и вторая перегруженная версия. Какой из них лучше?
Опять-таки, алгоритм разрешения перегрузки смотрит только на передаваемые аргументы и сравнивает их с типами аргументов всех методов кандидатов. Тип аргумента – Giraffe. У нас есть выбор: аргумент с типом Giraffe передается в метод с параметром типа Giraffe или аргумент с типом Giraffe передается в метод с параметром типа Animal. Совершенно очевидно, что первый вариант подходит лучше, поскольку существует точное соответствие типов.
Поэтому мы отбрасываем вторую перегрузку, поскольку она хуже другого кандидата. У нас остается только один кандидат, с точным соответствием типов аргументов и параметров. И только после разрешения перегрузки, мы проверяем, не происходит ли нарушения ограничения обобщенного метода.
Когда я пытаюсь объяснить это людям, они обычно приводят следующую часть спецификации в доказательство того, что я не прав, не прав, не прав:
Если F – обобщенный метод и метод M не имеет списка аргументов типа, метод F является кандидатом в случае:
1) успешного вывод типа и
2) после замены соответствующих параметров метода выведенными типами аргументов, все типы в списке параметров метода Fудовлетворяют своим ограничениям, и список параметров метода F является применимым с точки зрения А. [выделено автором]
С первого взгляда может показаться, что метод Foo<Giraffe> не может быть кандидатом, поскольку ограничения метода не удовлетворяются. Но все это происходит только от неправильного понимания спецификации: фрагмент «в списке параметров метода» означает формальный список параметров, а не список типов параметров метода.
Давайте я для ясности приведу пример, в котором эти правила вступают в силу. Предположим мы имеем:
class C<T> where T : Mammal {}
…
static void Bar<T>(T t, C<T> c) {}
static void Bar(Animal animal, string s) { }
…
Bar(new Iguana(), null);
При выводе типов мы получаем, что метод Bar<Iguana> может быть кандидатом. Но это также означает, что мы вызываем метод, который преобразовывает null к типу C<Iguana>. Поэтому результаты вывода типов отбрасываются и Bar<Iguana> не добавляется в список кандидатов.
Но если эта часть спецификации не является важной, тогда какая же является? Важная составляющая описана после описания определения самого подходящего метода, а не до этого.
Если лучшим методом является обобщенный метод, типы аргументов (указанные явно или выведенные) проверяются на соответствие ограничений этого обобщенного метода. Если типы аргументов не удовлетворяют соответствующим ограничениям типа параметра, происходит ошибка компиляции.
Обычно людей приводит в недоумение тот факт, что некорректный метод может быть выбран в качестве лучшего метода, вместо корректного метода, но менее подходящего по типу параметров. (*) Это принцип разрешения перегрузки (и вывода аргументов типа) находит лучшее соответствие списка аргументов формальному списку параметров каждого метода кандидата. Т.е., поиск производится по сигнатуре методов кандидатов. Если же лучшее соответствие аргументов метода сигнатуре находит метод, который по какой-то причине нельзя вызвать, тогда вам следует выбирать аргументы тщательнее, чтобы неверный метод больше не являлся самым подходящим. Мы решили, что лучше сказать вам об этой проблеме, чем тихо вернуться к менее подходящему выбору.
UPDATE: Моя коллега Джен, любитель серии книг «Сумерки» (Twilight), о которой я упоминал несколько постов назад, заметила, что это не только хороший дизайн языка, но это также отличный совет для свиданий. Если самое лучшее соответствие вашему критерию среди доступных ребят на Match.com определяет парней, которым, по какой-то причине невозможно дозвониться, тогда вам нужно выбрать критерий более тщательно, чтобы плохой парень вдруг не оказался лучшим кандидатом. По этим словам стоит жить.