【问题标题】:Enumerator Implementation: Use struct or class?枚举器实现:使用结构还是类?
【发布时间】:2010-09-27 22:10:14
【问题描述】:

我注意到List<T> 将其枚举器定义为struct,而ArrayList 将其枚举器定义为class。有什么不同?如果我要为我的班级编写一个枚举器,哪个更可取?

编辑:使用yield 无法满足我的要求,因此我正在实现自己的枚举器。也就是说,我想知道按照List<T> 的行并将其实现为结构是否会更好。

【问题讨论】:

  • 我非常想知道您在使用 yield return 时遇到的问题。
  • 1) 我非常讨厌“编译器魔法”,我不确定输出会是什么。 (虽然我会在简单的情况下使用它。)
  • 2) 我想支持一个可以在迭代过程中修改的列表。正常的迭代器语义禁止这样做。 (而且我不知道编译器怎么知道我的列表被修改了!)
  • 那么几个无害的问题:学习编译器的魔法不是更好吗,这样您就可以从中受益?当说你的实现速度更快、内存消耗更少时,你是否在实际测试中进行了测量?您多久使用一次重置?
  • 对于List<T> 部分,请在此问题中了解 Eric Lippert 的解释:why-bcl-collections-use-struct-enumerators-not-classes?

标签: c# .net enumeration


【解决方案1】:

扩展@Earwicker:您通常最好编写枚举器类型,而是使用yield return 让编译器为您编写它。这是因为如果您自己动手,您可能会错过许多重要的细微之处。

有关如何使用它的更多详细信息,请参阅 SO 问题“What is the yield keyword used for in C#?”。

Raymond Chen 也有一系列博文(“The implementation of iterators in C# and its consequences”:部分1234)向您展示如何在没有@987654329 的情况下正确实现迭代器@,它显示了它有多复杂,以及为什么你应该只使用yield return

【讨论】:

    【解决方案2】:

    像其他人一样,我会选择一个班级。可变结构很讨厌。 (正如 Jared 所建议的,我会使用一个迭代器块。手动编码一个枚举器是很复杂的。)

    有关列表枚举器是导致问题的可变结构的示例,请参见 this thread...

    【讨论】:

    • 嗯,这个例子证明我应该把它变成一个类。 :) 但是为什么 List 是这样实现的呢?为什么没有更正?
    • 现在改变它肯定为时已晚 - 但我不知道为什么它首先是这样设计的。大概是以效率的名义 - 但这是一个糟糕的电话,IMO。
    • 我推测它是这样的,因此只需将枚举器复制到另一个变量,就可以“保存”枚举器的当前状态 - 我最近自己使用这个“功能”来做链表切片(如LinkedList.Enumerator 也是一个结构体)
    • 示例链接已失效,Way Back Machine 也无济于事。
    • @JonSkeet:我猜如果语言的设计是为了让他们在检查名为 GetEnumerator 的方法之前先查找名为 DuckTypeGetEnumerator 之类的方法,那么 List<T>.DuckTypeGetEnumerator 会返回一个结构,List<T>.GetEnumerator 将返回一个类。让foreach 在内部使用结构只有好处,只有让IEnumerator<T>.GetEnumerator 返回一个结构的坏处;但是,让显式实现的接口返回一个类而类方法返回一个结构会很奇怪。
    【解决方案3】:

    IEnumerable<T> 的任何实现都应该返回一个类。出于性能原因,有一个 GetEnumerator 方法可能很有用,该方法返回一个提供枚举所需方法但不实现 IEnumerator<T> 的结构;这个方法应该不同于IEnumerable<T>.GetEnumerator,然后应该显式地实现。

    当使用 C# 或 vb.net 中的 foreach 或“For Each”循环或任何执行枚举的代码将知道枚举器是结构的上下文中枚举类时,使用此方法将提高性能,但要避免在枚举数被装箱并按值传递时会发生的陷阱。

    【讨论】:

    • 值得注意的是,这种级别的性能提升在大多数日常应用程序中并不重要。在深入研究结构枚举器之前,开发人员应该使用分析器来发现真正的问题点。尽管如此,它仍然是一种有效的技术,我已经使用了几次,效果很好。
    • IEnumerator 在 WPF 的 PathFigureCollection 中是 implemented as a struct。这是一个具体的例子,当它有一个返回结构的GetEnumerator方法对性能有用时
    • @zwcloud:当与foreach 循环结合使用时,出于性能原因,返回结构的公共GetEnumerator() 方法的存在将很有用。使用结构而不是类来实现IEnumerator<T>.GetEnumerator() 通常是相对无害的。我不知道在任何情况下,将返回结构的 GetEnumerator() 函数作为 IEnumerator<T>.GetEnumerator() 的实现会比让后一个函数返回类更好;更有可能的是,性能下降足够小......
    • ...MS 决定为这两个目的使用一种方法(和枚举器类型)。
    【解决方案4】:

    原因列表使用结构枚举器是为了防止在 foreach 语句中产生垃圾。这是一个很好的理由,特别是如果您正在为 Compact Framework 进行编程,因为 CF 没有分代 GC,并且 CF 通常用于性能低下的硬件,这会很快导致性能问题。

    另外,我不认为可变结构是一些发布的示例中问题的根源,但程序员对值类型的工作原理没有很好的理解。

    【讨论】:

    • 我看不出struct 枚举器如何防止垃圾生成。如果它是class,您是否更有可能得到同一个枚举器的多个副本,而不是单个枚举器对象?还是不需要对值类型进行垃圾回收?
    • 值类型的垃圾收集方式与引用类型不同。引用类型存在于堆中,而值类型存在于栈中。因此,虽然通过压缩 GC 堆来释放引用类型(如果你有大而复杂的堆,可能会很慢);通过弹出堆栈(非常快)释放值类型。
    • 通常你不需要担心垃圾生成,因为 GC 会足够快,不会成为性能瓶颈。但在高频代码等特殊情况下,堆上的大量分配会导致 GC 比平时更频繁地运行,从而导致性能问题。 (例如在游戏中的每一帧分配大量新对象)
    【解决方案5】:

    有几个 blog posts 正好涵盖了这个问题。基本上,枚举器结构是一个非常糟糕的主意......

    【讨论】:

    • 如果有一个干净的方法让它与IEnumerable<T>.GetEnumerator 返回的类型不同,那么将foreach 使用的东西作为一个结构体将没有什么好处。有趣的是,如果不是用于变量类型推断,GetEnumerator 是否返回一个结构或类并不重要,因为将返回值分配给IEnumerator<T> 有效地将它变成一个类对象(尽管一个有点-损坏的Equals 方法)。问题在于var myEnumerator=thing.GetEnumerator();
    【解决方案6】:

    使用yield return 编写它。

    至于为什么您可能会在classstruct 之间进行选择,如果您将其设为struct,那么它会在作为接口返回后立即被装箱,因此将其设为struct 会导致进行额外的复制。看不出有什么意义!

    【讨论】:

    • 哪个强制问题:为什么 List 使用结构枚举器?
    • 在有人想出它有利的理由之前,我们将不得不假设负责它的 Microserf 暂时疯了。
    【解决方案7】:

    在 C# 中编写枚举器的最简单方法是使用“yield return”模式。例如。

    public IEnumerator<int> Example() {
      yield return 1;
      yield return 2;
    }
    

    此模式将在后台生成所有枚举器代码。这使您无法做出决定。

    【讨论】:

      【解决方案8】:

      枚举器本质上是一个不断变化的结构,因为它需要更新内部状态才能移动到原始集合中的下一个值。

      在我看来,结构应该是不可变的,所以我会使用一个类。

      【讨论】:

        猜你喜欢
        • 2013-05-06
        • 1970-01-01
        • 2016-02-29
        • 1970-01-01
        • 2011-03-11
        • 1970-01-01
        • 2013-01-11
        • 1970-01-01
        • 2011-12-04
        相关资源
        最近更新 更多