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

本文关键字:内存 分配 循环 foreach | 更新日期: 2023-09-27 18:29:56

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

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

例如,在System.Collections.Generic.List<T>类上使用Reflector,下面是GetEnumerator:的实现

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

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

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

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

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

C#编译器使用鸭子类型来查找GetEnumerator()方法,并在可能的情况下使用它。System.Collection.Generic类型上的大多数GetEnumerator()方法都有返回结构的GetEnumerator()方法,并且对数组进行了特殊处理。如果GetEnumerator()方法不进行分配,通常可以避免进行分配。

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


注意:自从Unity 5.5更新到C#6以来,我知道目前没有任何编译器版本仍然有第二个分配

还有第二种分配,理解起来有点复杂。进行foreach循环:

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

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

List<int>.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<int>.Enumerator是一个结构,不需要在堆上分配,但强制转换enumerator as System.IDisposable将该结构框起来,这是一个分配。C#5.0更改了规范,禁止分配,但.NET打破了规范,提前优化了分配。

这些拨款非常少。请注意,分配与内存泄漏非常不同,使用垃圾回收,您通常不必担心它。然而,在某些情况下,您甚至会关心这些分配。我做Unity3D的工作,在5.5之前,我们不能在每个游戏帧的操作中进行任何分配,因为当垃圾收集器运行时,会出现明显的倾斜。

请注意,数组上的foreach循环是经过特殊处理的,不必调用Dispose。据我所知,foreach在数组上循环时从未进行过分配。

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

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

不过,其他集合的枚举器可能是类,它们会在堆上分配对象。您需要为每种情况检查枚举器的类型才能确定。

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

如注释中所述,这通常不应该是您需要担心的问题,因为这是垃圾回收的重点。也就是说,我的理解是,是的,每个foreach循环都将生成一个新的Enumerator对象,该对象最终将被垃圾收集。要了解原因,请查看此处的接口文档。正如您所看到的,有一个函数调用请求下一个对象。这样做的能力意味着枚举器有状态,并且必须知道下一个是哪个。至于为什么这是必要的,图像你正在引起收集项目的每个排列之间的交互:

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

在这里,同一集合上同时有两个枚举器。很明显,共享位置无法实现您的目标。