Why do C# Arrays use a reference type for Enumeration, but List<T> uses a mutable struct?
根据我读到的内容,出于性能原因,对某些集合的枚举器类型作出了设计决策,将其作为可变结构而不是引用类型。枚举器是最有名的。
我正在研究一些使用数组的旧代码,并惊讶地发现C数组返回szGenericArrayEnumerator类型作为其泛型枚举器类型,这是一种引用类型。
我想知道是否有人知道为什么当许多其他性能关键的集合使用可变结构时,数组的泛型迭代器被实现为引用类型。
From what I've read, a design decision was made for certain Collections's Enumerator Types to be mutable structs instead of reference types for performance reasons.
号
好问题。
首先,你是对的。虽然一般来说,可变值类型是一种糟糕的代码味道,但在这种情况下,它们是合理的:
- 变异几乎完全被用户隐藏。
- 任何人都不太可能以令人困惑的方式使用枚举器。
- 使用可变值类型实际上解决了非常常见的场景中的实际性能问题。
I am wondering if anyone knows why Array's generic iterator was implemented as a reference type when so many other performance critical collections used mutable structs instead.
号
因为如果您是那种关心数组枚举性能的人,那么为什么首先使用枚举器呢?它是一个数组,看在上帝的份上;只需编写一个for循环,它像普通人一样重复其指示,从不分配枚举器。(或者一个foreach循环;如果C编译器知道循环集合是一个数组,它会将foreach循环重写为等效的for循环。)
首先,从数组中获取枚举器的唯一原因是,如果要将它传递给一个采用IEnumerator的方法,在这种情况下,如果枚举器是一个结构,那么无论如何都要对它进行装箱。为什么要承担制作值类型然后装箱的费用?只需将其作为引用类型开始。
- 考虑到LINQ的普遍性(和表达能力),很可能经常会检索到数组的枚举器。例如,对于许多属性/方法,.NET中的反射类返回数组-并且使用Linq w/Reflection非常方便。据我所知,大多数LINQ运算符不执行特殊情况检查,以确定它们所处理的IEnumerable<>是否实际上是一个数组。
- @卢布什金:对。如果您不愿意承担堆分配和垃圾收集枚举器的成本,那么您也可能不愿意承担堆分配和垃圾收集查询的成本,以及它创建的所有枚举器对象,以及必须创建的所有委托。更不用说调用该查询的所有开销(检查参数是否正确,等等)。linq-to对象的性能是合理的,但它的设计肯定不是为了最小化堆分配!LINQ在整个地方分配堆内存。
- 另一个因素,我怀疑,原来的List的枚举器是在仿制药之前设计的。List.Enumerator的结构只影响三种类型的代码:显式使用List.Enumerator类型的代码,接受IEnumerable约束的泛型参数的代码,或使用var声明一个变量来保存列表的枚举器的代码。如果程序员要指定List.Enumerator,程序员应该知道他在做什么。至于其他两种类型的代码,它们在设计List.Enumerator时是不存在的。
- 虽然List.Enumerator在仿制药发明后得到了明确的发展,但最不令人惊讶的原则将有利于其工作方式与List.Enumerator相同,即使仿制药的发展有时意味着某些角落案例的行为可能比它们看起来更奇怪。
- @supercat:您是正确的,更一般地说,foreach的整个业务是基于模式的,而不是调用IEnumerable方法,主要是为了避免前泛型强类型集合中的拳击惩罚。在我们从v1开始使用泛型的反事实世界中,发明值类型迭代器的动力要少得多。
- @ericlippert:即使使用泛型,使用值类型迭代器也可以避免每次输入"foreach"循环时创建堆对象。不过,在某些方面,如果编译器可以查找foreach_GetEnumerator方法的存在,可能会更好;公共GetEnumerator方法可以返回枚举器类,而foreach_GetEnumerator返回结构。这似乎是两全其美。
- @Ericlippert:"或者是foreach循环;如果C编译器知道循环集合是一个数组,它会将foreach循环重写为等效的for循环",我从来不知道!?是否有文件记录在案(不是我不相信),我有兴趣了解它
- 对于其他阅读本文的人,这个答案涵盖了编译器stackoverflow.com/a/1124763/4500所做的优化(决定不要懒惰,自己去搜索;-)
- @supercat,你提到的这个非通用的List.Enumerator是什么?你是说ArrayList.ArrayListEnumerator吗?如果是这样,那是一个类,而不是一个结构。
- @我是罪犯。如果ArrayListEnumerator是一个类,那么决定让List.Enumerator成为一个更有趣的结构。我以为有个类在泛型之前使用了枚举器结构,但也许没有。在任何情况下,只有在极少数的角落情况下,List.Enumerator的结构才会引起任何问题;在非常常见的情况下,这是有利的。
数组在C编译器中得到一些特殊的处理。当在它们上使用foreach时,编译器将其转换为for循环。因此,使用struct枚举器没有性能优势。
另一方面,List是一个没有任何特殊处理的普通类,因此使用结构可以获得更好的性能。
- 不是所有foreach都编译成for循环吗?
- @oskarkjellin no,正常的foreach循环类似于while(enumerator.MoveNext()){var element=enumerator.Current;...},而foreach循环数组索引进入数组(for(int i=0;i)。
- @Oskarkjellin:所有的foreach和for循环最终被编译成while循环。codeinchaos的观点是,数组上的foreach循环实际上首先被编译成传统的for (int i = 0; i < array.Length; ++i)样式的循环,而不是大多数foreach循环编译成的while(enumtor.MoveNext())样式的循环。
- @Ericlippert现在你说,考虑到IEnumerator接口和它的方法,这很明显。不是真的在想:(