C # 4.0 «динамический» и оператор foreach - PullRequest
11 голосов
/ 30 мая 2010

Незадолго до того, как я обнаружил, это новое ключевое слово 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 # полностью игнорирует, что коллекция из динамически типизированной переменной может реализовывать интерфейс IEnumerable<T>!

Полное поведение оператора foreach описано в спецификации C # 4.0 8.8.4 Оператор foreach статья.

Но ... Совершенно возможно реализовать то же поведение во время выполнения! Можно добавить дополнительный флаг CSharpBinderFlags.ForEachCast, исправить полученный код так:

foreach (int x in (IEnumerable<int>) /* dynamic cast with the CSharpBinderFlags.ForEachCast flag */ (object) new Foo()) {
    ...
}

И добавить дополнительную логику к CSharpConvertBinder:

  • Wrap IEnumerable коллекций и IEnumerator до IEnumerable<T> / IEnumerator<T>.
  • Коллекции Wrap не реализуют Ienumerable<T> / IEnumerator<T> для реализации этих интерфейсов.

Таким образом, сегодня оператор foreach выполняет итерации по dynamic, полностью отличному от итерации по статически известной переменной коллекции, и полностью игнорирует информацию о типе, заданную пользователем. Все, что приводит к другому поведению итерации (IEnumarble<T> -определяющие коллекции, повторяется как только IEnumerable -определяющее) и более чем 150x замедление при итерации по dynamic. Простое исправление приведет к гораздо лучшей производительности:

foreach (int x in (IEnumerable<int>) dynamicVariable) {

Но почему я должен писать такой код?

Очень приятно видеть, что иногда C # 4.0 dynamic работает совершенно одинаково, если тип будет известен во время компиляции, но очень печально видеть, что dynamic работает совершенно иначе, когда IT CAN работает так же, как статически типизированный код.

Итак, мой вопрос: почему foreach над dynamic работает иначе, чем foreach над чем-либо еще?

Ответы [ 2 ]

23 голосов
/ 30 мая 2010

Прежде всего, чтобы объяснить некоторую предысторию читателям, которые смущены вопросом: язык C # фактически не требует, чтобы коллекция "foreach" реализовывала IEnumerable. Скорее, требуется либо реализация IEnumerable, либо реализация IEnumerable<T>, , либо просто наличие у него метода GetEnumerator (и что метод GetEnumerator возвращает что-то с Current и MoveNext, совпадающим с ожидаемый рисунок и т. д.)

Это может показаться странной функцией для статически типизированного языка, такого как C #. Почему мы должны "соответствовать шаблону"? Почему бы не потребовать , чтобы коллекции реализовывали IEnumerable?

Подумайте о мире перед дженериками. Если вы хотите создать коллекцию целых, вам придется использовать 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, но желает предложить прокси-объект, который делает это, он может сделать это бесплатно.

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

2 голосов
/ 30 мая 2010

Но почему я должен писать такой код?

Действительно. И зачем компилятору писать такой код? Вы удалили все шансы предположить, что цикл можно оптимизировать. Кстати, вы, похоже, неправильно интерпретируете IL, он перепривязывается для получения IEnumerable.Current, вызов MoveNext () является прямым, а GetEnumerator () вызывается только один раз. Что я считаю уместным, следующий элемент может или не может привести к int без проблем. Это может быть коллекция разных типов, каждый со своим связующим.

...