为啥 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<T>
的方法,在这种情况下如果枚举器是一个结构,那么您'无论如何都要拳击它。为什么要承担制作值类型然后装箱的费用?只需将其作为引用类型开始即可。
【讨论】:
鉴于 LINQ 的普遍性(和表达能力),可能会经常为数组检索枚举数。例如,.NET 中的反射类为许多属性/方法返回数组——使用带反射的 LINQ 非常方便。据我所知,大多数 LINQ 运算符不会执行特殊情况检查来确定他们正在处理的 IEnumerable 是否实际上是一个数组。 @LBushkin:正确。如果您不愿意承担堆分配和垃圾收集枚举器的成本,那么您可能也不愿意承担堆分配和垃圾收集的成本查询,以及它创建的所有枚举器对象,以及您必须创建的所有委托。更不用说调用该查询的所有开销(检查参数是否正确,等等)。 LINQ to objects 的性能相当不错,但它肯定不是为了最小化堆分配而设计的! LINQ 在各处分配堆内存。 我怀疑另一个因素是原始List
的枚举器是在泛型出现之前设计的。 List.Enumerator
的结构性只影响三种类型的代码:显式使用 List.Enumerator
类型的代码,接受约束为 IEnumerable
的泛型参数的代码,或使用 var
声明将持有的变量的代码列表的枚举器。如果程序员要指定List.Enumerator
,程序员应该知道他在做什么。至于其他两种代码,在设计List.Enumerator
时是不存在的。
虽然List<T>.Enumerator
显然是在泛型发明之后开发的,但最小惊讶原则将有利于它以与List.Enumerator
相同的方式工作,即使泛型的开发意味着某些角落-case 行为有时可能看起来比其他情况更奇怪。
@supercat:你是对的,更一般地说,foreach
的整个业务是基于模式而不是仅调用 IEnumerable
方法主要是为了避免强类型前泛型中的拳击惩罚收藏品。在我们从 v1 开始使用泛型的反事实世界中,发明值类型迭代器的动力要小得多。【参考方案2】:
数组在 C# 编译器中得到一些特殊处理。当您在它们上使用foreach
时,编译器会将其转换为for
循环。因此,使用struct
枚举器没有性能优势。
另一方面,List<T>
是一个没有任何特殊处理的普通类,因此使用结构可以获得更好的性能。
【讨论】:
不是所有foreach都编译成for循环了吗? @OskarKjellin 不,正常的foreach
循环变成类似于while(enumerator.MoveNext())var element=enumerator.Current;...
而foreach
循环遍历数组索引到数组中(for(int i=0;i<arr.Length;i++)
。
@OskarKjellin:所有foreach
和for
循环最终编译成while
循环。 CodeInChaos 的观点是,数组上的foreach
循环实际上首先编译成传统的for (int i = 0; i < array.Length; ++i)
样式循环,而不是大多数foreach
循环编译成的while(enumtor.MoveNext())
样式循环。
@EricLippert 现在您说考虑到 IEnumerator 接口及其方法,这一点非常明显。没想到:(以上是关于为啥 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构?的主要内容,如果未能解决你的问题,请参考以下文章
将 Enumeration<Integer> for 循环从 Java 转换为 C#? C# 中的 Enumeration<Integer> 到底是啥? [复制]
调用方法并将返回值分配给数组时,为啥C#在调用方法时使用数组引用?