30

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

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

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

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

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

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

4

4 回答 4

71

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

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

但是,如果您正在处理接口IEnumerableIEnumerable<T>或. 即使您的实现类返回一个结构,该结构也将被装箱并强制转换为or ,这需要分配。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();
}

WhileList<int>.Enumerator是一个结构,不需要在堆上分配,强制转换enumerator as System.IDisposable将结构装箱,这是一个分配。规范随着 C# 5.0 更改,禁止分配,但 .NET打破了规范并更早地优化了分配。

这些分配非常少。请注意,分配与内存泄漏非常不同,使用垃圾收集,您通常不必担心它。但是,在某些情况下,您甚至会关心这些分配。我从事 Unity3D 工作,直到 5.5,我们无法在每个游戏帧发生的操作中进行任何分配,因为当垃圾收集器运行时,您会遇到明显的颠簸。

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

于 2014-09-29T23:32:45.360 回答
3

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

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

其他集合的枚举器可能是类,它会在堆上分配一个对象。您需要检查每种情况的枚举数类型以确定。

于 2013-08-31T21:06:31.843 回答
0

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

于 2013-08-31T20:53:43.177 回答
0

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

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

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

于 2013-08-31T20:56:27.093 回答