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

15

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

Краткая версия. Почему компилятор не может определить тип времени компиляции 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 возвращают один и тот же тип?

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

    
задан jason 21.02.2012 в 18:24
источник
  • Любое, что связано с динамикой, полностью зависит от спецификации, пока оно не будет передано другому. Dynamic является заразным и расширяется, чтобы сделать все, что использует динамический, - стать динамичным. –  Marc Gravell♦ 21.02.2012 в 18:28
  • Да, я знаю. Вопрос в том, почему спецификация написана таким образом, даже если для выражения существует только один возможный тип. Здесь мы рассмотрим пример выражения вызова. –  jason 21.02.2012 в 18:29
  • Я не думаю, что это причина, на самом деле это даже никогда не рассматривалось во время проектирования, но сохранение динамического результата предотвращает распаковку и повторное использование, если результатом является тип значения, а остальная часть метода продолжается что приводит к другим вызовам динамических методов. –  hvd 21.02.2012 в 18:47

4 ответа

22

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

  

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

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

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

  

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

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

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

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

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

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

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

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

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

// 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?

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

Или, что, если вместо этого 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! Как разработчик, я бы нашел это совершенно неожиданным.

Причина три:

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

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

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

    
ответ дан Eric Lippert 21.02.2012 в 22:29
  • Учитывая наилучшие ответы на этот вопрос, вы можете получить динамический объект при десериализации строки JSON. В этом случае вы можете передать этот динамический объект одному методу (без перегрузок), который возвращает другой статически типизированный объект (сопоставление). Я считаю странным, что объект, возвращаемый этим методом, считается динамическим, хотя в описании метода указано иначе. –  Luca Cremonesi 02.11.2016 в 15:34
3

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

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

    
ответ дан Daniel 21.02.2012 в 20:22
1
  

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

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

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

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

  

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

    
ответ дан Reed Copsey 21.02.2012 в 18:33
  • ", но языковая команда решила не работать так." Это то, о чем я спрашиваю, я спрашиваю, почему это было спроектировано именно так, так что это полицейский, чтобы сказать, что это ответ. «Компилятор явно не выполняет статическое связывание (которое потребуется для того, чтобы получить нужное вам поведение) для динамических выражений». Опять же, это именно то, о чем я прошу; почему он был спроектирован именно так. «Я подозреваю, что ответ Эрика Липперта здесь применим». Я уверен, что ответ всегда применяется. Я надеюсь получить немного больше информации. –  jason 21.02.2012 в 18:37
  • @ Джейсон Возможно, кто-то из языковой команды решит перезвонить, но я подозреваю, что ответ будет тем же самым ответом, который они всегда дают: «потому что никто никогда не проектировал, не определял, не реализовывал, не тестировал, не документировал и не отправлял эту функцию». Я согласен, это хорошая идея, но так много других функций языка, которых они не делали - я думаю, они просто либо не думали об этом, либо решили, что не стоит пытаться реализовать его таким образом ... , –  Reed Copsey 21.02.2012 в 18:40
  • @ Рид Копси: Это тоже полицейский. Он не отвечает на вопрос о том, почему спецификация не записывается, чтобы набирать выражение как статический тип, когда существует только один возможный тип, независимо от того, существуют ли динамические составляющие. Я не прошу об обходах. Я не прошу о дешевых ответах. Ответ на каждый «почему» вопрос «потому что»; это не значит, что мы не спрашиваем «почему», потому что часто случаются интересные объяснения, скрывающиеся на заднем плане. –  jason 21.02.2012 в 19:40
  • Фактически в этом конкретном случае мы проектировали, специфицировали, реализовали и протестировали эту функцию. Мы все равно его отрезали, потому что это было ужасно. –  Eric Lippert 21.02.2012 в 22:29
  • @ Джейсон Часто бывает, что одна из причин отказа от данной функции - это наличие простого обходного пути (и, как объяснил Эрик, обходной путь в этом случае является даже лучшим решением, чем сама функция). Если функция опущена, потому что есть простой способ обхода проблемы, то не совсем точно ответить на вопрос «почему эта функция не существует», говоря «потому что есть простой способ обхода». –  phoog 21.02.2012 в 23:16
Показать остальные комментарии
1

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

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

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

ответ дан jbtule 21.02.2012 в 20:09