Можно ли повторно использовать коллекции IEnumerable более одного раза? - PullRequest
10 голосов
/ 08 февраля 2011

В основном мне интересно, можно ли использовать перечисление более одного раза в коде впоследствии. Независимо от того, прерываете ли вы раньше или нет, перечисление всегда будет сбрасываться в каждом случае foreach, указанном ниже, что дает нам последовательное перечисление от начала до конца набора эффектов?

var effects = this.EffectsRecursive;

foreach (Effect effect in effects)
{
...
}

foreach (Effect effect in effects)
{
    if(effect.Name = "Pixelate")
        break;
}

foreach (Effect effect in effects)
{
...
}

РЕДАКТИРОВАТЬ: реализация EffectsRecursive это:

public IEnumerable<Effect> Effects
{
    get
    {
        for ( int i = 0; i < this.IEffect.NumberOfChildren; ++i )
        {
            IEffect effect = this.IEffect.GetChildEffect ( i );
            if ( effect != null )
                yield return new Effect ( effect );
        }
    }
}

public IEnumerable<Effect> EffectsRecursive
{
    get
    {
        foreach ( Effect effect in this.Effects )
        {
            yield return effect;
            foreach ( Effect seffect in effect.ChildrenRecursive )
                yield return seffect;
        }
    }
}

Ответы [ 5 ]

14 голосов
/ 08 февраля 2011

Код, который использует последовательность, в порядке. Как указывает spender, код, генерирующий перечисление, может иметь проблемы с производительностью, если дерево глубокое.

Предположим, в самой глубокой точке ваше дерево имеет четыре глубины; подумайте о том, что происходит с четырьмя глубокими узлами. Чтобы получить этот узел, вы выполняете итерацию корня, который вызывает итератор, который вызывает итератор, который вызывает итератор, который передает узел обратно в код, который передает узел обратно в код, который передает узел обратно ... Вместо того, чтобы просто Передавая узел вызывающей стороне, вы создали небольшую бригаду с четырьмя парнями, и они перемещают данные от объекта к объекту, прежде чем он, наконец, попадает в цикл, который его хотел.

Если дерево всего четыре в глубину, вероятно, ничего страшного. Но предположим, что дерево состоит из десяти тысяч элементов и имеет тысячу узлов, образующих связанный список сверху, а остальные девять тысяч узлов снизу. Теперь, когда вы повторяете эти девять тысяч узлов, каждый из них должен пройти тысячу итераторов, всего девять миллионов копий, чтобы получить девять тысяч узлов. (Конечно, вы, вероятно, получили ошибку переполнения стека и также потерпели крах процесса.)

Способ решения этой проблемы, если он у вас есть, - это управлять стеком самостоятельно, а не помещать в стек новые итераторы.

public IEnumerable<Effect> EffectsNotRecursive() 
{     
    var stack = new Stack<Effect>();
    stack.Push(this);
    while(stack.Count != 0)
    {
        var current = stack.Pop();
        yield return current;
        foreach(var child in current.Effects)
            stack.Push(child);
    }
}

Исходная реализация имеет временную сложность O (nd), где n - количество узлов, а d - средняя глубина дерева; поскольку d в худшем случае может быть O (n), а в лучшем случае O (lg n), это означает, что алгоритм находится между O (n lg n) и O (n ^ 2) во времени. Это O (d) в пространстве кучи (для всех итераторов) и O (d) в пространстве стека (для всех рекурсивных вызовов.)

Новая реализация имеет временную сложность O (n), O (d) в пространстве кучи и O (1) в пространстве стека.

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

        foreach(var child in current.Effects.Reverse())

вместо.

Более подробный анализ этой проблемы см. В статье моего коллеги Уэса Дайера на эту тему:

http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx

13 голосов
/ 08 февраля 2011

Да, это законно. Шаблон IEnumerable<T> предназначен для поддержки нескольких перечислений в одном источнике. Коллекции, которые можно перечислить только один раз, вместо этого должны выставлять IEnumerator.

6 голосов
/ 08 февраля 2011

законно, да. Будет ли он работать так, как вы ожидаете, зависит от:

  • Реализация IEnumerable, возвращаемая EffectsRecursive и всегда ли он возвращает один и тот же набор;

  • Если вы хотите перечислить один и тот же набор оба раза

Если он возвращает IEnumerable, который требует некоторой интенсивной работы, и не кеширует результаты внутри, тогда вам может понадобиться .ToList() самостоятельно. Если он кеширует результаты, то ToList() будет немного избыточен, но, вероятно, не причинит вреда.

Кроме того, если GetEnumerator() реализован типичным / правильным (*) способом, то вы можете безопасно перечислить любое количество раз - каждый foreach будет новым вызовом GetEnumerator() который возвращает новый экземпляр IEnumerator. Но может быть в том, что в некоторых ситуациях он возвращает тот же экземпляр IEnumerator, который уже был частично или полностью перечислен, так что все это действительно зависит только от конкретного предполагаемого использования этого конкретного IEnumerable.

* Я уверен, что возвращение одного и того же перечислителя несколько раз на самом деле является нарушением подразумеваемого контракта для шаблона, но я видел некоторые реализации, которые все равно это делают.

1 голос
/ 08 февраля 2011

Скорее всего, да.Большинство реализаций IEnumerable возвращают свежий IEnumerator, который начинается в начале списка.

0 голосов
/ 08 февраля 2011

Все зависит от реализации типа EffectsRecursive.

...