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

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

Хотя с помощью динамического ключевого слова можно определить очень много вещей, существуют некоторые ограничения в отношении его использования. Хотя они не являются ограничителями показа, знайте, что динамический элемент данных не может использовать лямбда-выражения или анонимные методы 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;

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

3 ответа

Решение

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

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

d.M(123)

где d - выражение динамического типа. * Какой объект должен быть передан во время выполнения в качестве аргумента для вызова сайта "М"? Ясно, мы вставляем 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 и иметь возможность правильно сообщать, является ли любое возможное использование оператора using законным или незаконным.

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

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

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

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

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

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

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

Это заявление C#

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

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

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

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

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

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

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

Другие вопросы по тегам