为啥 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构?

Posted

技术标签:

【中文标题】为啥 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构?【英文标题】:Why do C# Arrays use a reference type for Enumeration, but List<T> uses a mutable struct?为什么 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构? 【发布时间】:2012-02-29 08:00:46 【问题描述】:

根据我的阅读,出于性能原因,已将某些集合的枚举器类型设计为可变结构而不是引用类型。 List.Enumerator 是最著名的。

我正在研究一些使用数组的旧代码,并惊讶地发现 C# 数组返回类型 SZGenericArrayEnumerator 作为其泛型枚举类型,这是一个引用类型。

我想知道是否有人知道为什么 Array 的泛型迭代器被实现为引用类型,而许多其他性能关键集合使用可变结构代替。

【问题讨论】:

【参考方案1】:

根据我的阅读,出于性能原因,已将某些集合的枚举器类型设计为可变结构而不是引用类型。

好问题。

首先,你是对的。虽然一般来说,可变值类型是一种不好的代码味道,但在这种情况下它们是合理的:

突变几乎对用户完全隐藏。 极不可能有人会以令人困惑的方式使用枚举器。 使用可变值类型实际上确实解决了极其常见场景中的实际性能问题。

我想知道是否有人知道为什么 Array 的泛型迭代器被实现为引用类型,而许多其他性能关键集合使用可变结构代替。

因为如果你是那种关心枚举数组的性能的人,那么你为什么首先使用枚举器?它是一个数组 看在上帝的份上;只需编写一个for 循环,像普通人一样迭代其指标,并且从不分配枚举器。 (或者 foreach 循环;如果 C# 编译器知道循环集合是一个数组,它会将 foreach 循环重写为等效的 for 循环。)

首先从数组中获取枚举器的唯一原因是,如果您将它传递给采用IEnumerator&lt;T&gt; 的方法,在这种情况下如果枚举器是一个结构,那么您'无论如何都要拳击它。为什么要承担制作值类型然后装箱的费用?只需将其作为引用类型开始即可。

【讨论】:

鉴于 LINQ 的普遍性(和表达能力),可能会经常为数组检索枚举数。例如,.NET 中的反射类为许多属性/方法返回数组——使用带反射的 LINQ 非常方便。据我所知,大多数 LINQ 运算符不会执行特殊情况检查来确定他们正在处理的 IEnumerable 是否实际上是一个数组。 @LBushkin:正确。如果您不愿意承担堆分配和垃圾收集枚举器的成本,那么您可能也不愿意承担堆分配和垃圾收集的成本查询,以及它创建的所有枚举器对象,以及您必须创建的所有委托。更不用说调用该查询的所有开销(检查参数是否正确,等等)。 LINQ to objects 的性能相当不错,但它肯定不是为了最小化堆分配而设计的! LINQ 在各处分配堆内存。 我怀疑另一个因素是原始List 的枚举器是在泛型出现之前设计的。 List.Enumerator 的结构性只影响三种类型的代码:显式使用 List.Enumerator 类型的代码,接受约束为 IEnumerable 的泛型参数的代码,或使用 var 声明将持有的变量的代码列表的枚举器。如果程序员要指定List.Enumerator,程序员应该知道他在做什么。至于其他两种代码,在设计List.Enumerator时是不存在的。 虽然List&lt;T&gt;.Enumerator 显然是在泛型发明之后开发的,但最小惊讶原则将有利于它以与List.Enumerator 相同的方式工作,即使泛型的开发意味着某些角落-case 行为有时可能看起来比其他情况更奇怪。 @supercat:你是对的,更一般地说,foreach 的整个业务是基于模式而不是仅调用 IEnumerable 方法主要是为了避免强类型前泛型中的拳击惩罚收藏品。在我们从 v1 开始使用泛型的反事实世界中,发明值类型迭代器的动力要小得多。【参考方案2】:

数组在 C# 编译器中得到一些特殊处理。当您在它们上使用foreach 时,编译器会将其转换为for 循环。因此,使用struct 枚举器没有性能优势。

另一方面,List&lt;T&gt; 是一个没有任何特殊处理的普通类,因此使用结构可以获得更好的性能。

【讨论】:

不是所有foreach都编译成for循环了吗? @OskarKjellin 不,正常的foreach 循环变成类似于while(enumerator.MoveNext())var element=enumerator.Current;...foreach 循环遍历数组索引到数组中(for(int i=0;i&lt;arr.Length;i++) @OskarKjellin:所有foreachfor 循环最终编译成while 循环。 CodeInChaos 的观点是,数组上的foreach 循环实际上首先编译成传统的for (int i = 0; i &lt; array.Length; ++i) 样式循环,而不是大多数foreach 循环编译成的while(enumtor.MoveNext()) 样式循环。 @EricLippert 现在您说考虑到 IEnumerator 接口及其方法,这一点非常明显。没想到:(

以上是关于为啥 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构?的主要内容,如果未能解决你的问题,请参考以下文章

将 Enumeration<Integer> for 循环从 Java 转换为 C#? C# 中的 Enumeration<Integer> 到底是啥? [复制]

为啥OpenGL不支持多索引缓冲?

为啥 pandas 多索引数据帧切片看起来不一致?

调用方法并将返回值分配给数组时,为啥C#在调用方法时使用数组引用?

Enumeration和Iterator是接口类,为啥能产生对象

添加对 C# 项目的引用以使其与版本控制兼容的正确方法