在C#中使用foreach循环时的内存分配

我知道关于foreach循环如何在C#中工作的基础知识( foreach循环如何在C#中工作 )

我想知道是否使用foreach分配可能导致垃圾收集的内存? (适用于所有内置的系统类型)。

例如,在System.Collections.Generic.List类上使用Reflector,这里是GetEnumerator的实现:

 public Enumerator GetEnumerator() { return new Enumerator((List) this); } 

在每次使用时,这会分配一个新的枚举器(以及更多的垃圾)。

所有类型都这样做吗? 如果是这样,为什么? (不能重用一个枚举器吗?)

Foreach可以导致分配,但至少在较新版本的.NET和Mono中,如果您正在处理具体的System.Collections.Generic类型或数组,则不会。 这些编译器的旧版本(例如Unity3D使用的Mono版本直到5.5)始终生成分配。

C#编译器使用duck类型来查找GetEnumerator()方法,并在可能的情况下使用它。 System.Collection.Generic类型上的大多数GetEnumerator()方法都有GetEnumerator()方法返回结构,并且数组是专门处理的。 如果您的GetEnumerator()方法没有分配,通常可以避免分配。

但是,如果您正在处理其中一个接口IEnumerableIEnumerableIListIList ,您将始终获得分配。 即使您的实现类返回一个结构,该结构也将被装箱并转换为IEnumeratorIEnumerator ,这需要分配。


注意:由于Unity 5.5已更新为C#6,因此我知道当前没有第二次分配的编译器版本。

有一个第二个分配,要理解起来有点复杂。 采取这个foreach循环:

 List valueList = new List() { 1, 2 }; foreach (int value in valueList) { // do something with value } 

直到C#5.0,它扩展到这样的东西(有一些小的差异):

 List.Enumerator enumerator = valueList.GetEnumerator(); try { while (enumerator.MoveNext()) { int value = enumerator.Current; // do something with value } } finally { IDisposable disposable = enumerator as System.IDisposable; if (disposable != null) disposable.Dispose(); } 

虽然List.Enumerator是一个结构,并且不需要在堆上分配,但是enumerator as System.IDisposable List.Enumerator enumerator as System.IDisposable器是struct,它是一个分配。 规范改变了C#5.0,禁止分配,但.NET 破坏了规范并更早地优化了分配。

这些分配非常小。 请注意,分配与内存泄漏非常不同,并且通过垃圾回收,您通常不必担心它。 但是,在某些情况下,您甚至不关心这些分配。 我做Unity3D工作直到5.5,我们无法在每个游戏框架的操作中进行任何分配,因为当垃圾收集器运行时,你会得到明显的晃动。

请注意,数组上的foreach循环是专门处理的,不必调用Dispose。 所以据我所知,foreach在循环数组时从未分配过。

不,枚举列表不会导致垃圾收集。

List类的枚举器不从堆中分配内存。 它是一个结构,而不是一个类,所以构造函数不分配对象,它只返回一个值。 foreach代码会将该值保留在堆栈上,而不是堆上。

其他集合的枚举器可能是类,它们会在堆上分配一个对象。 您需要检查每个案例的枚举器类型是否确定。

因为枚举器保持当前项目。 它就像一个游标与数据库相比。 如果多个线程访问同一个枚举器,您将失去对序列的控制。 每次foreach咨询时,你都必须将它重置为第一个项目。

正如评论中所提到的,这通常不应该是您需要担心的问题,因为这是垃圾收集的重点。 也就是说,我的理解是,每个foreach循环都会生成一个新的Enumerator对象,最终会被垃圾回收。 要了解原因,请查看此处的界面文档。 如您所见,有一个函数调用请求下一个对象。 执行此操作的能力意味着枚举器具有状态,并且必须知道下一个是哪个。 至于为什么这是必要的,图像你导致集合项的每个排列之间的交互:

 foreach(var outerItem in Items) { foreach(var innterItem in Items) { // do something } } 

在这里,您可以同时在同一个集合中使用两个枚举器。 显然,共享位置无法实现您的目标。