Не долгое время, прежде чем я обнаружил, настолько новый 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
поведение оператора описано в спецификации 8.8.4 C# 4.0 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) {
Но почему я должен написать код как это?
Это должно очень приятно иногда видеть это C# 4.0 dynamic
работы полностью то же, если тип будет известен во время компиляции, но это должно очень печально видеть это dynamic
работы, полностью отличающиеся, где IT CAN работает то же кодом со статическим контролем типов.
Таким образом, мой вопрос: почему foreach
dynamic
работы, отличающиеся от foreach
по чему-либо еще?
Прежде всего, чтобы объяснить некоторую предысторию читателям, которых смутил вопрос: язык C# на самом деле не требует, чтобы коллекция "foreach" реализовывала IEnumerable
. Скорее, он требует, чтобы она реализовала IEnumerable
, или чтобы она реализовала IEnumerable
, или просто чтобы у нее был метод GetEnumerator (и чтобы метод GetEnumerator возвращал что-то с Current и MoveNext, соответствующее ожидаемому шаблону, и так далее)
Это может показаться странной особенностью для статически типизированного языка, каким является C#. Почему мы должны "соответствовать шаблону"? Почему бы не потребовать, чтобы коллекции реализовывали IEnumerable?
Подумайте о мире до появления generics. Если бы вы хотели создать коллекцию ints, вам пришлось бы использовать IEnumerable. И, следовательно, при каждом вызове Current, коллекция будет содержать int, а затем, конечно же, вызывающая сторона будет немедленно разворачивать ее обратно в int. Что медленно и создает нагрузку на GC. Используя подход, основанный на паттернах, вы можете создавать сильно типизированные коллекции в C# 1.0!
Сейчас, конечно, никто не реализует этот паттерн; если вам нужна сильно типизированная коллекция, вы реализуете IEnumerable
и готово. Если бы система общих типов была доступна в C# 1.0, маловероятно, что функция "соответствия шаблону" была бы реализована в первую очередь.
Как вы заметили, вместо поиска шаблона код, сгенерированный для динамической коллекции в foreach ищет динамическое преобразование к IEnumerable (а затем делает преобразование от объекта, возвращаемого Current, к типу переменной цикла, конечно). Так что ваш вопрос, по сути, звучит так: "Почему код, сгенерированный при использовании динамического типа в качестве типа коллекции в foreach, не ищет шаблон во время выполнения?".
Потому что сейчас уже не 1999 год, и даже когда это было так во времена C# 1.0, коллекции, использующие паттерн, почти всегда также реализовывали IEnumerable. Вероятность того, что реальный пользователь будет писать код производственного качества на C# 4.0, который выполняет foreach над коллекцией, реализующей паттерн, но не IEnumerable, крайне мала. Если вы оказались в такой ситуации, что ж, это неожиданно, и мне жаль, что наш дизайн не смог предугадать ваши потребности. Если вы считаете, что ваш сценарий на самом деле распространен, и что мы неправильно оценили, насколько он редок, пожалуйста, опубликуйте более подробную информацию о вашем сценарии, и мы рассмотрим возможность его изменения для гипотетических будущих версий.
Обратите внимание, что преобразование, которое мы генерируем в IEnumerable, является динамическим преобразованием, а не просто проверкой типа. Таким образом, динамический объект может участвовать; если он не реализует IEnumerable, но хочет предложить прокси-объект, который реализует, он может свободно это сделать.
Короче говоря, дизайн "динамического foreach" - это "динамически запрашивать у объекта последовательность IEnumerable", а не "динамически выполнять все операции проверки типов, которые мы бы сделали во время компиляции". Теоретически это тонко нарушает принцип проектирования, согласно которому динамический анализ дает тот же результат, что и статический, но на практике именно так мы ожидаем, что подавляющее большинство коллекций с динамическим доступом будет работать.
Но почему я должен писать такой код?
Действительно. И зачем компилятору писать такой код? Вы устранили любую возможность предположить, что цикл можно оптимизировать. Кстати, вы, кажется, неправильно интерпретируете IL, он выполняет повторную привязку для получения IEnumerable.Current, вызов MoveNext () является прямым, а GetEnumerator () вызывается только один раз. Я думаю, что это уместно, следующий элемент может без проблем привести или не привести к типу int. Это может быть набор разных типов, каждый со своей папкой.