Почему выражение вызова метода имеет тип динамический, даже если существует только один возможный тип возврата?

Вдохновленный этим вопросом .

Краткая версия. Почему компилятор не может определить тип времени M(dynamic arg) для компиляции, если существует только одна перегрузка M или все перегрузки M имеют одинаковый тип возвращаемого значения?

В спецификации, §7.6.5:

Вызывающее выражение динамически связано (§7.2.2), если выполняется хотя бы одно из следующих условий:

  • Первичное выражение имеет динамический тип времени компиляции.

  • Как минимум один аргумент необязательного аргумента-списка имеет динамический тип времени компиляции, а первичное выражение не имеет типа делегата.

Имеет смысл, что для

class Foo {
    public int M(string s) { return 0; }
    public string M(int s) { return String.Empty; }
}

компилятор не может определить тип времени компиляции

dynamic d = // dynamic
var x = new Foo().M(d);

потому что он не будет знать до тех пор, пока не будет запущена перегрузка M.

Однако почему компилятор не может определить тип времени компиляции, если M имеет только одну перегрузку или все перегрузки M возвращают один и тот же тип?

Я хочу понять, почему спецификация не позволяет компилятору вводить эти выражения статически во время компиляции.

Ответы

Ответ 1

UPDATE: этот вопрос был тема моего блога 22 октября 2012 года. Спасибо за отличный вопрос!


Почему компилятор не может определить тип типа M(dynamic_expression) типа компиляции, если есть только одна перегрузка M или все перегрузки M имеют одинаковый тип возврата?

Компилятор может определить тип времени компиляции; тип времени компиляции является динамическим, и компилятор успешно показывает это.

Я думаю, что заданный вами вопрос:

Почему тип M(dynamic_expression) времени компиляции всегда динамичен, даже в редком и маловероятном случае, когда вы делаете совершенно ненужный динамический вызов методу M, который всегда будет выбран независимо от типа аргумента?

Когда вы формулируете такой вопрос, он сам отвечает.: -)

Причина одна:

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

object d = whatever;
Foo foo = new Foo();
int x = (d is string) ? foo.M((string)d) : foo((int)d);

Очевидно, что если есть только одна перегрузка M, то это еще проще: нарисуйте объект на нужный тип. Если это не удастся во время выполнения, потому что это плохое, ну, динамика тоже потерпела бы неудачу!

В этих сценариях просто нет необходимости в динамической динамике, поэтому зачем нам делать много дорогостоящих и сложных операций вывода типа в компиляторе, чтобы включить сценарий, который мы не хотим использовать динамическим для первое место?

Причина вторая:

Предположим, мы действительно сказали, что разрешение перегрузки имеет очень специальные правила, если группа методов статически известна, что она содержит один метод. Отлично. Теперь мы просто добавили новый тип хрупкости в язык. Теперь добавление новой перегрузки изменяет тип возвращаемого вызова на совершенно другой тип - тип, который не только вызывает динамическую семантику, но также и типы значений. Но подождите, все становится хуже!

// Foo corporation:
class B
{
}

// Bar corporation:
class D : B
{
    public int M(int x) { return x; }
}

// Baz corporation:
dynamic dyn = whatever;
D d = new D();
var q = d.M(dyn);

Предположим, что мы реализуем вашу функцию requiest и делаем вывод, что q является int, по вашей логике. Теперь корпорация Foo добавляет:

class B
{
    public string M(string x) { return x; }
}

И вдруг, когда корпорация Baz перекомпилирует свой код, внезапно тип q спокойно переходит в динамический, потому что во время компиляции мы не знаем, что dyn не является строкой. Это странное и неожиданное изменение в статическом анализе! Почему третье лицо, добавляющее новый метод в базовый класс, вызывает изменение типа локальной переменной в совершенно другом методе в совершенно другом классе, написанном в другой компании, компании, которая даже не использует B напрямую, но только через D?

Это новая форма проблемы с хрупким базовым классом, и мы стремимся минимизировать проблемы с хрупким базовым классом в С#.

Или, что, если вместо этого Foo corp сказал:

class B
{
    protected string M(string x) { return x; }
}

Теперь, по вашей логике,

var q = d.M(dyn);

дает q тип int, когда код выше находится за пределами типа, который наследуется от D, но

var q = this.M(dyn);

дает тип q как динамический, если внутри типа, который наследуется от D! Как разработчик, я бы нашел это совершенно неожиданным.

Причина три:

Слишком много умений на С#. Наша цель - не создавать логический движок, который мог бы выработать все возможные ограничения типа для всех возможных значений, заданных конкретной программой. Мы предпочитаем иметь общие, понятные, понятные правила, которые можно легко записать и реализовать без ошибок. Спецификация уже составляет восемьсот страниц, и писать компилятор без ошибок невероятно сложно. Пусть не усложнится. Не говоря уже о расходах на тестирование всех этих сумасшедших случаев.

Причина четыре:

Кроме того: язык предоставляет вам много возможностей для использования анализатора статического типа. Если вы используете динамический, вы , в частности, просим, ​​чтобы этот анализатор отложил свое действие до времени выполнения. Не следует удивляться, что использование функции "перестать делать статический тип во время компиляции" приводит к тому, что анализ статического типа не очень хорошо работает во время компиляции.

Ответ 2

Ранняя конструкция функции dynamic имела поддержку чего-то подобного. Компилятор по-прежнему будет выполнять статическое разрешение перегрузки и введет "перегрузку phantom", которая будет представлять динамическое разрешение перегрузки только при необходимости.

Как вы можете видеть во втором сообщении, этот подход вводит большую сложность (во второй статье рассказывается о том, как нужно вводить вывод типа для того, чтобы этот подход работал). Я не удивлен, что команда С# решила пойти с более простой идеей всегда использовать динамическое разрешение перегрузки, когда участвует dynamic.

Ответ 3

Однако почему компилятор не может определить тип времени компиляции, если M имеет только одну перегрузку или все перегрузки M возвращают один и тот же тип?

Компилятор мог бы это сделать, но языковая команда решила не работать так.

Вся цель dynamic состоит в том, чтобы иметь все выражения с использованием динамического выполнения с "их разрешение отложено до запуска программы" (С# spec, 4.2.3). Компилятор явно не выполняет статическую привязку (которая потребуется для получения желаемого здесь поведения) для динамических выражений.

Имея отказ от статической привязки, если был только один параметр привязки, заставил компилятор проверить этот случай - который не был добавлен. Что касается того, почему языковая команда не хотела этого делать, я подозреваю, что Ответ Эрика Липперта здесь:

Меня спрашивают: "Почему С# не реализует функцию X?" все время. Ответ всегда один и тот же: поскольку никто не проектировал, не определял, не реализовывал, не тестировал, не документировал и не отправлял эту функцию.

Ответ 4

Я думаю, что случай, когда можно статически определять единственный возможный тип возвращаемого значения разрешения динамического метода, настолько узкий, что он был бы более запутанным и непоследовательным, если бы это сделал компилятор С#, вместо того, чтобы иметь дело с поведением платы.

Даже с вашим примером, если Foo является частью другой dll, Foo может быть более новой версией во время выполнения из перенаправления привязки с дополнительным M, которые имеют другой тип возвращаемого значения, а затем компилятор допустил бы неправильный результат, потому что разрешение во время выполнения возвращало бы другой тип.

Что делать, если Foo является IDynamicMetaObjectProvider, d может не соответствовать ни одному из статических аргументов и, таким образом, он откажется от динамического поведения, которое может возвращать другой тип.