Ограничения динамического типа в 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
дело не хватает этого важного контекста.