Группа из одного метода

На этой неделе я реализовывал семантический анализ динамических выражений в проекте Roslyn, так что я исследовал множество вопросов со своей командой, касающихся дизайна динамических возможностей в C# 4. В этом контексте мне очень часто задают следующий вопрос:

public class Alpha
{
public int Foo(string x) { ... }
}
...
dynamic d = whatever;
Alpha alpha = MakeAlpha();
var result = alpha.Foo(d);

Как осуществляется анализ этого кода? А более точно вопрос заключается в том, какой тип локальной переменной result?

Если тип приемника (т.е. переменная alpha) вызова метода является типом dynamic, тогда мы мало что можем сделать на этапе компиляции. Мы проанализируем типы аргументов времени компиляции и сгенерируем вызов динамического объекта, что приведет к переносу семантического анализа во время исполнения на основе типов времени исполнения динамического выражения. Но в данном случае это не так. Во время компиляции мы знаем тип приемника. Один из ключевых принципов проектирования динамических возможностей языка C# заключается в том, что если мы точно знаем тип во время компиляции, то он учитывается при анализе во время исполнения. Другими словами, мы используем тип времени исполнения только в том случае, если он действительно является динамическим; во всех остальных случаях мы используем тип времени компиляции. Если MakeAlpha() возвращает класс-наследник от Alpha и этот класс содержит несколько перегруженных версий метода Foo, то нам все равно.

Поскольку мы знаем, что собираемся выполнять анализ перегрузки методов для вызова метода Foo экземпляра типа Alpha, то мы можем выполнить «санитарную проверку» (sanity check) во время компиляции, и убедиться, что мы не упадем гарантированно во время исполнения. Таким образом, мы выполняем разрешение перегрузки, но вместо исполнения полного алгоритма (устранения неподходящих кандидатов, определения уникального наиболее подходящего кандидата, выполнения окончательной валидации кандидата), мы выполняем частичный алгоритм. Мы отсекаем неподходящие кандидаты и если у нас остается один или более кандидатов, тогда мы используем динамическую привязку метода. Если у нас не остается кандидатов, то мы выдаем сообщение об ошибке, поскольку у нас точно ничего не будет работать во время выполнения.

Теперь может возникнуть вполне резонный вопрос: механизм перегрузки методов может определить, что в группе методов (method group) существует лишь один подходящий кандидат, а значит, мы можем статически определить, что тип результата будет int, так почему мы говорим, что типом результата является dynamic?

Вопрос кажется разумным, но давайте еще немного подумаем об этом. Если вы знаете, я знаю, и компилятор знает о том, что механизм перегрузки выберет конкретный метод, то зачем вы вообще делаете динамический вызов? Почему бы не преобразовать d к типу string? Это редкая ситуация, у которой есть простое обходное решение путем добавления соответствующего преобразования типов (преобразование выражения вызова к int, или аргумента к string). Редкие, маловероятные ситуации, для которых есть простое обходное решение, являются плохими кандидатами для оптимизации в компиляторе. Вы просите динамический вызов, вот вы его и получаете.

Этого вполне достаточно, чтобы не добавлять предложенную возможность, но давайте рассмотрим этот вопрос глубже и исследуем варианты упомянутого ранее сценария. Eta Corporation создала следующий класс:

public class Eta {}

И Zeta Corporation расширила его следующим образом:

public class Zeta : Eta
{
public int Foo(string x){ ... }
}
...
dynamic d = whatever;
Zeta zeta = new Zeta();
var result = zeta.Foo(d);

Предположим, мы знаем, что типом результата является int, поскольку группа методов состоит лишь из одного кандидата. Теперь предположим, что в новой версии Eta Corporation добавила новый метод:

public class Eta
{
public string Foo(double x){...}
}

Zeta Corporation перекомпилировала свой код, и, вуаля, тип возвращаемого значения внезапно изменился на dynamic! Почему изменения, сделанные Eta Corporation в базовом классе должны изменить семантический анализ класса-наследника? Это кажется неожиданным. Язык C# был тщательно спроектирован, чтобы избежать такого рода проблем, называемых «проблемами Хрупких Базовых Классов» (“Brittle Base Class” failures); см. мои другие примеры по этой теме.

Эту ситуацию можно еще ухудшить. Предположим, Eta Corporation внесла вместо этого такие изменения:

public class Eta
{
protected string Foo(double x){...}
}

Что будет теперь? Должен ли тип результата быть int при вызове снаружи класса Zeta, поскольку алгоритм перегрузки дает лишь один доступный кандидат, но будет типом dynamic при вызове изнутри класса, поскольку кандидатов будет два? Такое поведение будет еще более странным.

Слишком хитрое решение с очень небольшим выхлопом. Нас просили сделать динамический вызов, вот его мы и выполняем; поэтому и результат должен быть типом dynamic. Выгоды от статического вывода типов динамических выражений не перевешивают недостатки, поэтому мы и не пытаемся этого делать. Если вам нужен статический анализ, так просто не выключайте его.

В следующий раз: динамические проблемы при выводе типов методов.