С# 4.0 "динамический" и оператор foreach

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

Полное поведение оператора foreach описано в спецификации С# 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:

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

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

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

Но зачем мне писать такой код?

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

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

Ответ 1

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

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

Подумайте о мире перед дженериками. Если вы хотите создать коллекцию int, вам придется использовать IEnumerable. И поэтому каждый вызов Current будет содержать int, и, конечно, вызывающий пользователь сразу же распакует его обратно в int. Это медленно и создает давление на GC. Перейдя на шаблонный подход, вы можете создавать строго типизированные коллекции в С# 1.0!

В настоящее время, конечно, никто не реализует этот шаблон; если вам нужна строго типизированная коллекция, вы реализуете IEnumerable<T>, и все готово. Если для С# 1.0 была доступна система общего типа, маловероятно, что функция "соответствовать шаблону" была бы реализована в первую очередь.

Как вы уже отметили, вместо поиска шаблона код, созданный для динамической коллекции в foreach, ищет динамическое преобразование в IEnumerable (а затем выполняется преобразование из объекта, возвращаемого Current, в тип Конечно, ваш вопрос в основном состоит в том, "почему код, созданный при использовании динамического типа в качестве типа коллекции foreach, не может найти шаблон во время выполнения?"

Поскольку он больше не работает в 1999 году, и даже когда он вернулся в С# 1.0, коллекции, которые использовали шаблон, также почти всегда выполняли IEnumerable. Вероятность того, что настоящий пользователь будет писать код качества С# 4.0, который делает foreach над коллекцией, которая реализует шаблон, но не IEnumerable, крайне низок. Теперь, если вы в такой ситуации, ну, это неожиданно, и мне жаль, что наш дизайн не смог предвидеть ваши потребности. Если вы считаете, что ваш сценарий на самом деле распространен, и что мы недооценили его, насколько это редко, отправьте более подробную информацию о своем сценарии, и мы рассмотрим возможность изменения этого для будущих гипотез.

Обратите внимание, что преобразование, которое мы генерируем в IEnumerable, представляет собой динамическое преобразование, а не просто тест типа. Таким образом, может участвовать динамический объект; если он не реализует IEnumerable, но хочет предложить прокси-объект, который делает это, он может это сделать.

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

Ответ 2

Но зачем мне писать такой код?

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