【问题标题】:Is it OK to reuse IEnumerable collections more than once?可以多次重复使用 IEnumerable 集合吗?
【发布时间】:2011-02-08 00:24:36
【问题描述】:

基本上我想知道是否可以在随后的代码中多次使用枚举。无论您是否提前中断,枚举是否会在以下每个 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;
        }
    }
}

【问题讨论】:

  • 顺便说一句,我添加了 EffectsRecursive 的实现。
  • 当心递归收益率。这里有很多编译器魔法在创建状态机。如果数据结构很大,这可能会比您预期的性能更低/更复杂。
  • @spender:谢谢,我可以使用其他方法吗?
  • @Eric 在下面的回答比我能做的更有说服力。

标签: c# .net ienumerable


【解决方案1】:

是的,这样做是合法的。 IEnumerable&lt;T&gt; 模式旨在支持单个源上的多个枚举。只能枚举一次的集合应该公开IEnumerator

【讨论】:

    【解决方案2】:

    使用序列的代码很好。正如 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())
    

    改为。

    有关此问题的更多分析,请参阅我的同事 Wes Dyer 关于该主题的文章:

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

    【讨论】:

    • @Eric:谢谢 Eric,这是一个了不起的答案。我将像这样更改我的递归枚举。但最后在你的例子中,你的例子,它将从 9000 个项目中最顶层的父项开始,然后从上到下遍历这 9000 个项目,然后从第一个最顶层开始遍历其他最顶层的父项我们作为第一个元素迭代的父元素,直到我们最终得到最左上角的元素?这就是“树从上到下,从右到左遍历”的意思吗?
    • @Eric:顺便说一句,你说“stack.Push(this);”的那一行也返回当前实例,对吗?在我的实现中,我只想返回当前效果的子效果而不返回自身。我可以删除它吗?我觉得这会导致 stack.Pop() 行出现问题。你怎么看?
    • @Joan:关于你的第一个问题:假设树是 A,有左孩子 B 和右孩子 C。这个算法按 A、C、B 的顺序产生它们。首先我们将 A 放入堆栈.然后我们弹出 A 并将 B 和 C 放入堆栈。然后我们弹出 C,然后我们弹出 B。B 和 C 是从右到左而不是从左到右完成的。如果你想要 A、B、C,那么在它们被推送之前反转子列表。推 A,推 A,推 C,推 B,推 B,推 C,现在我们有了 A、B、C。
    • @Joan:关于您的第二个问题:您可以做的是在进入循环之前将“this”的孩子推入堆栈以使球滚动。
    • @Joan:你忘了括号。我的是方法,你的是属性。
    【解决方案3】:

    合法,是的。它是否会按您的预期运行取决于:

    • EffectsRecursive返回的IEnumerable的实现以及是否总是返回同一个集合;

    • 是否要两次枚举同一个集合

    如果它返回一个需要大量工作的 IEnumerable,并且它没有在内部缓存结果,那么您可能需要自己 .ToList() 它。如果它确实缓存了结果,那么ToList() 会有点多余,但可能没有什么害处。

    此外,如果GetEnumerator()典型/正确 (*) 方式实现,那么您可以安全地枚举任意次数 - 每个foreach 都将是对@987654327 的新调用@ 返回IEnumerator 实例。但可能在某些情况下,它会返回相同的 IEnumerator 实例,该实例已被部分或全部枚举,因此这完全取决于特定 IEnumerable 的具体预期用途。

    *我很确定多次返回同一个枚举器实际上违反了该模式的隐含合同,但我已经看到了一些实现。

    【讨论】:

      【解决方案4】:

      很可能,是的。 IEnumerable 的大多数实现都返回一个新的 IEnumerator,它从列表的开头开始。

      【讨论】:

        【解决方案5】:

        这完全取决于EffectsRecursive的类型的实现。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-01-06
          • 2014-06-16
          • 1970-01-01
          • 1970-01-01
          • 2013-11-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多