Поскольку я – гик, мне нравится узнавать о порой тонких различиях между вещами, которые легко спутать. Например:
Я подумал, что мог бы провести серию экскурсов в трудноразличимые концепции в дизайне языков программирования.
Вот вопрос, который мне задают довольно часто:
public class C{public static void DoIt<T>(T t){ReallyDoIt(t);}private static void ReallyDoIt(string s){System.Console.WriteLine("строка");}private static void ReallyDoIt<T>(T t){System.Console.WriteLine("всё остальное");}}
Что происходит при вызове C.DoIt<string>? Многие люди ожидают, что выведется «строка» в то время, как на самом деле всегда печатается «всё остальное», независимо от того, какой T использовать.
Спецификация C# гласит, что когда у вас есть выбор между ReallyDoIt<string>(string) и ReallyDoIt(string) – то есть, когда выбор идет из двух методов с идентичными сигнатурами, но один из них получает эту сигнатуру после подстановки обобщённых параметров – тогда мы выбираем «натуральную» сигнатуру вместо «подстановленной». Почему в этом случае мы этого не делаем?
Потому, что это не тот выбор, который нам предоставлен.
Если бы вы сказали
ReallyDoIt("здравствуй, мир");
то мы бы выбрали «натуральную» версию. Но вы не передали чего-то, известного компилятору как строка. Вы передали что-то, известное как T, неограниченный тип-параметр, так что он может быть чем угодно. Стало быть, думает алгоритм разрешения перегрузок, есть ли у нас тут метод, который принимает всё, что угодно? Да, есть.
Это иллюстрирует то, что обобщения в C# не похожи на шаблоны в C++. Вы можете думать о шаблонах, как о продвинутом механизме поиска и замены. Когда вы говорите DoIt<string> в шаблоне, компилятор ищет все применения «T», заменяет их на «string», а потом компилирует получившийся исходный код. Разрешение перегрузок работает с подставленными типами-аргументами, и сгенерированный код отражает результаты этого разрешения.
Обобщения работают не так; обобщённые типы – они, ну, обобщённые. Мы выполняем разрешение перегрузок единожды и замораживаем результат. Мы не меняем его во время исполнения, когда кто-то, возможно в совсем другой сборке, использует строку как тип-аргумент для метода. Уже выбран метод, который будет вызваться в IL, сгенерированном нами для обобщённого типа. Компилятор JIT не говорит «ага, я тут случайно знаю, что, если бы мы попросили компилятор C# сейчас выполниться, имея эту дополнительную информацию, то он бы выбрал другой перегруженный метод. Дай-ка я перепишу код, исходно сгенерированный компилятором C#...». Компилятор JIT ничего не знает о правилах C#.
В сущности, пример выше не отличается от этого:
public class C{ public static void DoIt(object t) { ReallyDoIt(t); } private static void ReallyDoIt(string s) { System.Console.WriteLine("строка"); } private static void ReallyDoIt(object t) { System.Console.WriteLine("всё остальное"); }}
Когда компилятор генерирует код вызова ReallyDoIt, он выбирает версию с object потому, что это лучшее, что он может сделать. Если кто-то вызовет DoIt со строкой, то он всё равно пойдет в версию с object.
Теперь, если вы хотите, чтобы разрешение перегрузок было выполнено повторно во время исполнения, основываясь на реальных типах аргументов, то мы можем это для вас сделать; это как раз то, что делает новая функциональность «dynamic» в C# 4.0. Просто замените «object» на «dynamic», и когда вы сделаете вызов с участием этого объекта, мы запустим во время исполнения алгоритм разрешения перегрузок и динамически сгенерируем код для вызова того метода, который бы выбрал компилятор, знай он фактические типы во время компиляции.