Ограничения динамического типа в C#

Не могли бы вы указать причины ограничений динамического типа в C#? Я читал о них в "Pro C# 2010 и платформа .NET 4". Вот отрывок (если цитирование книг здесь незаконно, скажите мне, и я удалю отрывок):

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

dynamic a = GetDynamicObject(); 
// Error!  Methods on dynamic data can’t use lambdas! 
a.Method(arg => Console.WriteLine(arg));

Чтобы обойти это ограничение, вам нужно будет напрямую работать с базовым делегатом, используя методы, описанные в главе 11 (анонимные методы, лямбда-выражения и т. д.). Еще одно ограничение состоит в том, что динамическая точка данных не может понимать никакие методы расширения (см. главу 12). К сожалению, это также будет включать любые методы расширения, исходящие из API-интерфейсов LINQ. Поэтому переменная, объявленная с ключевым словом dynamic, имеет очень ограниченное применение в LINQ to Objects и других технологиях LINQ:

dynamic a = GetDynamicObject(); 
// Error!  Dynamic data can’t find the Select() extension method! 
var data = from d in a select d;

Заранее спасибо.


person Community    schedule 28.08.2010    source источник
comment
Это очевидно, динамический объект не имеет свойства с именем Method и не является IEnumarable. Я не вижу тех вещей, которые вы назвали ограничениями, возможно, вы и автор не поняли, что делает dynamic.   -  person BrunoLM    schedule 28.08.2010
comment
Я так не думаю. Компилятор не будет проверять достоверность членов (например, метода).   -  person    schedule 28.08.2010


Ответы (3)


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

Для лямбда-выражений ситуация на самом деле сложнее, чем простая проблема определения того, идет ли лямбда к дереву выражений или к делегату. Рассмотрим следующее:

d.M(123)

где d — выражение динамического типа. * Какой объект должен передаваться во время выполнения в качестве аргумента для сайта вызова «M»? Ясно, что мы боксируем 123 и пропускаем его. Затем алгоритм разрешения перегрузки в связующем времени выполнения просматривает тип времени выполнения d и тип времени компиляции int 123 и работает с этим.

А что, если бы это было

d.M(x=>x.Foo())

Какой же объект мы должны передать в качестве аргумента? У нас нет способа представить «лямбда-метод одной переменной, который вызывает неизвестную функцию с именем Foo для любого типа x».

Предположим, мы хотим реализовать эту функцию: что нам нужно будет реализовать? Во-первых, нам нужен способ представления несвязанной лямбда-выражения. Деревья выражений предназначены только для представления лямбда-выражений, в которых известны все типы и методы. Нам нужно изобрести новый вид «нетипизированного» дерева выражений. Затем нам нужно будет реализовать все правила привязки лямбда-выражений в связывателе времени выполнения.

Рассмотрим последний пункт. Лямбды могут содержать операторы. Для реализации этой функции требуется, чтобы компоновщик времени выполнения содержал полный семантический анализатор для каждого возможного оператора в C#.

Это было на порядок больше нашего бюджета. Мы бы до сих пор работали над C# 4, если бы захотели реализовать эту функцию.

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

ОБНОВЛЕНИЕ: комментарий просит разъяснения по поводу семантического анализатора.

Рассмотрим следующие перегрузки:

class C {
  public void M(Func<IDisposable, int> f) { ... }
  public void M(Func<int, int> f) { ... }
  ...
}

и звонок

d.M(x=> { using(x) { return 123; } });

Предположим, что d имеет динамический тип времени компиляции и тип времени выполнения C. Что должен делать компоновщик времени выполнения?

Связыватель времени выполнения должен определить во время выполнения, можно ли преобразовать выражение x=>{...} в каждый из типов делегатов в каждой из перегрузок M.

Для этого связующее время выполнения должно иметь возможность определить, что вторая перегрузка неприменима. Если бы это было применимо, вы могли бы иметь int в качестве аргумента для оператора использования, но аргумент для оператора использования должен быть одноразовым. Это означает, что компоновщик во время выполнения должен знать все правила для оператора using и быть в состоянии правильно сообщить, является ли любое возможное использование оператора using законным или незаконным.

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

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

person Eric Lippert    schedule 28.08.2010
comment
Большое спасибо за подробный ответ. Не могли бы вы объяснить эту часть. Рассмотрим последний пункт. Лямбды могут содержать операторы. Для реализации этой функции требуется, чтобы компоновщик времени выполнения содержал полный семантический анализатор для каждого возможного оператора C#. немного яснее? - person ; 28.08.2010

Лямбда-выражения. Я думаю, что одной из причин отказа от поддержки лямбда-выражений в качестве параметров динамических объектов является то, что компилятор не знает, компилировать ли лямбда-выражение как делегат или как дерево выражений.

Когда вы используете лямбду, компилятор принимает решение на основе типа целевого параметра или переменной. Когда это Func<...> (или другой делегат), он компилирует лямбду как исполняемый делегат. Когда целью является Expression<...>, лямбда-выражение компилируется в дерево выражений.

Теперь, когда у вас есть тип dynamic, вы не знаете, является ли параметр делегатом или выражением, поэтому компилятор не может решить, что делать!

Методы расширения: я думаю, что причина здесь в том, что поиск методов расширения во время выполнения был бы довольно сложным (и, возможно, также неэффективным). Прежде всего, среде выполнения необходимо знать, на какие пространства имен ссылаются с помощью using. Затем ему нужно будет искать все классы во всех загруженных сборках, фильтровать те, которые доступны (по пространству имен), а затем искать их для методов расширения...

person Tomas Petricek    schedule 28.08.2010
comment
Спасибо. Может ли среда выполнения решить, является ли параметр делегатом или выражением? И главная причина в том, что внедрение этих функций будет дорогостоящим? Кстати, впечатляет, что вы так молоды и написали книгу! - person ; 28.08.2010
comment
Тот факт, является ли параметр делегатом или выражением, должен быть известен компилятору (чтобы он мог правильно скомпилировать код). В принципе, компилятор мог бы сделать и то, и другое для динамических типов (и тогда среда выполнения могла бы решить), но я думаю, что это было бы слишком много работы... - person Tomas Petricek; 28.08.2010

Эрик (и Томас) хорошо это сказал, но вот как я об этом думаю.

Этот оператор С#

a.Method(arg => Console.WriteLine(arg)); 

не имеет смысла без множества контекста. Сами лямбда-выражения не имеют типов, они могут быть преобразованы в типы delegate (или Expression). Таким образом, единственный способ получить значение — предоставить некоторый контекст, который заставляет лямбду преобразовываться в конкретный тип делегата. Этот контекст обычно (как в этом примере) является разрешением перегрузки; учитывая тип a и доступные перегрузки Method для этого типа (включая члены расширения), мы можем поместить некоторый контекст, который придает лямбда-значение.

Без этого контекста для создания значения вам в конечном итоге придется объединять все виды информации о лямбде в надежде каким-то образом связать неизвестные во время выполнения. (Какой IL вы могли бы сгенерировать?)

В отличие от этого, если вы поместите туда конкретный тип делегата,

a.Method(new Action<int>(arg => Console.WriteLine(arg))); 

Казам! Все стало просто. Независимо от того, какой код находится внутри лямбда-выражения, теперь мы точно знаем, какой у него тип, а это значит, что мы можем скомпилировать IL так же, как тело любого метода (теперь мы, например, знаем, какую из многочисленных перегрузок Console.WriteLine мы вызываем ). И этот код имеет один конкретный тип (Action<int>), что означает, что компоновщику среды выполнения легко увидеть, есть ли у a Method, который принимает этот тип аргумента.

В C# голая лямбда почти бессмысленна. Лямбда-выражения C# нуждаются в статическом контексте, чтобы придать им смысл и исключить двусмысленность, возникающую из-за многих возможных преобразований и перегрузок. Типичная программа легко предоставляет этот контекст, но в случае dynamic этот важный контекст отсутствует.

person Brian    schedule 28.08.2010
comment
Я перечитываю свой ответ и понимаю, что тело Console.WriteLine(arg), вероятно, не является хорошим примером того, как будет выглядеть IL? сложный вопрос, потому что я думаю, что это просто - это похоже на IL, когда arg имеет тип dynamic. Итак, ответ Эрика полон лучших примеров того, что действительно сложно. Я думаю. Я тоже могу ошибаться. :) Тем не менее, я чувствую, что мой ответ передает суть в том виде, в каком он есть, поэтому я оставлю его как есть. - person Brian; 28.08.2010