C# 4.0 'dynamic' и оператор foreach
Незадолго до того, как я обнаружил, что новое ключевое слово dynamic плохо работает с оператором C#foreach:
using System;
sealed class Foo {
public struct FooEnumerator {
int value;
public bool MoveNext() { return true; }
public int Current { get { return value++; } }
}
public FooEnumerator GetEnumerator() {
return new FooEnumerator();
}
static void Main() {
foreach (int x in new Foo()) {
Console.WriteLine(x);
if (x >= 100) break;
}
foreach (int x in (dynamic)new Foo()) { // :)
Console.WriteLine(x);
if (x >= 100) break;
}
}
}
Я ожидал, что итерация над переменной dynamic должна работать полностью, как если бы тип переменной коллекции был известен во время компиляции. Я обнаружил, что второй цикл на самом деле выглядит так, когда компилируется:
foreach (object x in (IEnumerable) /* dynamic cast */ (object) new Foo()) {
...
}
И каждый доступ к переменной x приводит к динамическому поиску / приведению, поэтому C# игнорирует, что я указал правильный тип x в заявлении foreach - это было немного удивительно для меня... Кроме того, компилятор C# полностью игнорирует тот факт, что коллекция из динамически типизируемой переменной may реализует интерфейс IEnumerable<T>!
Полное поведение оператора
foreachописано в спецификации C# 4.0 8.8.4 в статье foreach statement.Но... Вполне возможно реализовать то же самое поведение во время выполнения! Можно добавить дополнительный флаг CSharpBinderFlags.ForEachCast, исправить эммитированный код to выглядит так:
foreach (int x in (IEnumerable<int>) /* dynamic cast with the CSharpBinderFlags.ForEachCast flag */ (object) new Foo()) {
...
}
И добавьте дополнительную логику к CSharpConvertBinder:
- оберните
IEnumerableколлекции иIEnumeratorвIEnumerable<T>/IEnumerator<T>. - Wrap collections не реализует
Ienumerable<T>/IEnumerator<T>для реализации этого интерфейса.
Поэтому сегодня оператор
foreachперебирает dynamic совершенно иначе, чем перебирает статически известную переменную коллекции, и полностью игнорирует информацию о типе, заданную пользователем. Все это приводит к различным итерациям поведение (IEnumarble<T>-реализующие коллекции повторяются как только IEnumerable-реализующие) и более чем 150x замедление при повторении dynamic. Простое исправление приведет к гораздо лучшей производительности: foreach (int x in (IEnumerable<int>) dynamicVariable) {
Но почему я должен писать такой код?
Очень приятно видеть, что иногда C# 4.0 dynamic работает совершенно одинаково, если тип будет известен во время компиляции, но очень печально видеть, что dynamic работает совершенно по-другому, где он может работать так же, как статически набрал код.
Итак, мой вопрос: почему
foreach over dynamic работает иначе, чем foreach над чем-либо другим? 2 ответов:
Во-первых, чтобы объяснить некоторые предпосылки читателям, которые смущены вопросом: язык C# на самом деле не требует, чтобы коллекция "foreach" реализовывала
IEnumerable. Скорее, он требует либо реализацииIEnumerable, либо реализацииIEnumerable<T>, или просто, что у него есть метод GetEnumerator (и что метод GetEnumerator возвращает что-то с текущим и MoveNext, что соответствует ожидаемому шаблону, и так далее.)Это может показаться странной особенностью для статически типизированный язык, как C#, чтобы иметь. Почему мы должны "соответствовать шаблону"? Почему не требуется, что коллекция реализует интерфейс IEnumerable?
Подумайте о мире до появления дженериков. Если вы хотите сделать коллекцию ints, вам придется использовать IEnumerable. И поэтому каждый вызов Current будет вставлять int, а затем, конечно, вызывающий немедленно распакует его обратно в int. Что является медленным и создает давление на GC. Используя подход, основанный на шаблонах, вы можете сделать строго типизированные коллекции в C# 1.0! В настоящее время, конечно, никто не реализует этот шаблон; если вы хотите строго типизированную коллекцию, вы реализуетеIEnumerable<T>, и все готово. Если бы универсальная система типов была доступна для C# 1.0, маловероятно, что функция" соответствовать шаблону " была бы реализована в первую очередь.Как вы уже заметили, вместо поиска шаблона код, сгенерированный для динамической коллекции в foreach , ищет динамическое преобразование в IEnumerable (и затем выполняет преобразование из объекта, возвращаемого текущим, В тип переменной цикла, конечно.) Итак, ваш вопрос в основном таков: "почему код, созданный с использованием динамического типа в качестве типа коллекции foreach, не ищет шаблон во время выполнения?"
Потому что сейчас уже не 1999 год, и даже когда это было в дни C# 1.0, коллекции, которые использовали шаблон, также почти всегда реализовывали IEnumerable. Вероятность того, что реальный пользователь является написание кода производственного качества C# 4.0, который делает foreach над коллекцией, реализующей шаблон, но не IEnumerable, чрезвычайно низко. Теперь, если вы находитесь в такой ситуации, что ж, это неожиданно, и я сожалею, что наш дизайн не смог предвидеть ваши потребности. Если вы считаете, что ваш сценарий на самом деле распространен, и что мы недооценили, насколько он редок, пожалуйста, опубликуйте более подробную информацию о вашем сценарии, и мы рассмотрим возможность изменения этого для гипотетических будущих версий.
Примечание что преобразование, которое мы генерируем в IEnumerable, являетсядинамическим преобразованием, а не просто тестом типа. Таким образом, динамический объект может участвовать; если он не реализует IEnumerable, но хочет предложить прокси-объект, который это делает, он свободен сделать это.
Короче говоря, дизайн "dynamic foreach" - это "динамически запрашивать объект для IEnumerable последовательности", а не "динамически выполнять каждую операцию тестирования типа, которую мы бы сделали во время компиляции". Это делает в теории тонко нарушая принцип проектирования, динамический анализ дает тот же результат, что и статический анализ, но на практике мы ожидаем, что подавляющее большинство динамически доступных коллекций будет работать именно так.
Но почему я должен писать такой код?
Действительно. И зачем компилятору писать такой код? Вы удалили любой шанс, что он мог бы догадаться, что цикл может быть оптимизирован. Кстати, вы, кажется, неправильно интерпретируете IL, это повторная привязка для получения IEnumerable.В настоящее время вызов MoveNext() является прямым, а GetEnumerator() вызывается только один раз. Что я считаю уместным, следующий элемент может быть или не быть брошен в int без проблем. Это может быть ... коллекция различных типов,каждый со своим собственным связующим.
Comments