在 .NET 中,使用“foreach”迭代 IEnumerable<ValueType> 的实例会创建一个副本吗?那么我应该更喜欢使用“for”而不是“foreach”吗?

Posted

技术标签:

【中文标题】在 .NET 中,使用“foreach”迭代 IEnumerable<ValueType> 的实例会创建一个副本吗?那么我应该更喜欢使用“for”而不是“foreach”吗?【英文标题】:In .NET, using "foreach" to iterate an instance of IEnumerable<ValueType> will create a copy? So should I prefer to use "for" instead of "foreach"? 【发布时间】:2011-08-05 13:08:21 【问题描述】:

在 .NET 中,使用“foreach”迭代 IEnumerable 的实例会创建一个副本吗?那么我应该更喜欢使用“for”而不是“foreach”吗?

我写了一些代码来证明这一点:

struct ValueTypeWithOneField

    private Int64 field1;


struct ValueTypeWithFiveField

    private Int64 field1;
    private Int64 field2;
    private Int64 field3;
    private Int64 field4;
    private Int64 field5;


public class Program

    static void Main(string[] args)
    
        Console.WriteLine("one field");
        Test<ValueTypeWithOneField>();

        Console.WriteLine("-----------");

        Console.WriteLine("Five field");
        Test<ValueTypeWithFiveField>();

        Console.ReadLine();
    

    static void Test<T>()
    
        var test = new List<T>();
        for (int i = 0; i < 5000000; i++)
        
            test.Add(default(T));
        

        Stopwatch sw = new Stopwatch();

        for (int i = 0; i < 5; i++)
        
            sw.Start();

            foreach (var item in test)
            

            

            sw.Stop();
            Console.WriteLine("foreach " + sw.ElapsedMilliseconds);
            sw.Restart();

            for (int j = 0; j < test.Count; j++)
            
                T temp = test[j];
            

            sw.Stop();
            Console.WriteLine("for " + sw.ElapsedMilliseconds);
            sw.Reset();
        
    

这是我运行代码后得到的结果:

    one field
    foreach 68
    for 72
    foreach 68
    for 72
    foreach 67
    for 72
    foreach 64
    for 73
    foreach 68
    for 72
    -----------
    Five field
    foreach 272
    for 193
    foreach 273
    for 191
    foreach 272
    for 190
    foreach 271
    for 190
    foreach 275
    for 188

正如我们在结果中看到的,“foreach”总是比“for”花费更多的时间。

那么在遍历值类型的通用集合时,我应该更喜欢使用“for”而不是“foreach”吗?

注意:感谢提醒,我编辑了代码和结果。但是,foreach 的运行速度仍然比 for 慢。

【问题讨论】:

我不认为你的测试是正确的。 foreach 将为 item 分配一个值,但 for 循环不分配任何内容。如果你真的在 for 循环中做一个赋值会发生什么: var k = test[j]; 我认为,为了让这个测试更准确,你应该对实际元素做一些简单的事情。喜欢var l = item + 1; / var l = test[i] + 1。此外,您必须在 foreach 循环中调用 GetEnumerator 函数,而 for 循环仅计算而不触及列表。 @Tokk: item++ 是不允许的。您不能修改迭代器变量。测试需要等效的操作,这意味着for 块需要检索值。 谢谢,我刚刚编辑了代码和结果。 @CuiPengFei:这个结果更准确,但这里有什么意义呢?您应该使用对您更有意义的任何循环结构。即使一个具有性能优势,在枚举 500 万个成员集合 之后,您的结果也会显示出约 50 毫秒的差异。你说的是微不足道的时间。 【参考方案1】:

你的问题太复杂了。分解一下。

使用“foreach”迭代一系列值类型是否会创建该序列的副本?

没有。

使用“foreach”迭代一系列值类型是否会创建每个值的副本?

是的。

使用“for”对值类型的索引序列进行等效迭代是否会创建每个值的副本?

通常,是的。如果您知道有关集合的特殊信息(例如它是一个数组),则可以采取一些措施来避免复制。但在索引集合的一般情况下,对序列进行索引会返回序列中值的副本,而不是对包含该值的存储位置的引用

对值类型进行任何操作是否会复制该值?

差不多。值类型按值复制。这就是为什么它们被称为值类型。您对不复制的值类型所做的唯一事情是调用值类型上的方法,并使用“out”或“ref”传递值类型变量。值类型被不断地复制;这就是为什么值类型通常比引用类型慢。

使用“foreach”或“for”来迭代引用类型序列是否会复制引用?

是的。引用类型的表达式的值是一个引用。每当使用该引用时,都会复制该引用。

那么就复制行为而言,值类型和引用类型之间有什么区别?

值类型是按值复制的。引用类型复制引用,但不复制被引用的事物。每次使用 16 字节值类型时都会复制 16 个字节。每次使用 16 字节引用类型时都会复制 4(或 8)字节引用。

foreach 循环比 for 循环慢吗?

通常是这样。 foreach 循环通常做更多的工作,因为它创建一个枚举器并调用枚举器上的方法,而不是仅仅增加一个整数。整数增量非常快。也不要忘记 foreach 循环中的枚举器必须被释放,这也需要时间。

我是否应该使用 for 循环而不是 foreach 循环,因为 for 循环有时会快几微秒?

没有。那是愚蠢的。您应该根据以客户为中心的经验数据做出明智的工程决策。 foreach 循环的额外负担很小。客户可能永远不会注意到。你应该做的是:

根据客户意见设定绩效目标 衡量您是否实现了目标 如果没有,使用分析器找出最慢的东西 修复它 重复直到达到目标

如果您遇到性能问题,将 foreach 循环更改为 for 循环不会对您的问题产生任何影响。首先以看起来清晰易懂的方式编写代码。

【讨论】:

+1 我开始喜欢你现在的样子 answering :) Skeet 在 Lippert 上一无所获。 :)【参考方案2】:

您的测试不准确;在foreach 版本中,您实际上是在旋转枚举器并从列表中检索每个值(即使您没有使用它)。在for 版本中,除了查看其Count 属性之外,您根本没有对列表做任何事情。您实际上是在测试遍历集合的枚举器与将整数变量递增相等次数的性能。

要创建奇偶校验,您需要声明一个临时变量并在 for 循环的每次迭代中分配它。

话虽如此,您的问题的答案是是的。每次赋值或return 语句都会创建一个值的副本。

性能

这个伪代码分解应该可以解释为什么 foreach 在这个特定实例中比使用 for 慢一些:

foreach:

try

    var en = test.GetEnumerator(); //creates a ListEnumerator
    T item;

    while(en.MoveNext()) // MoveNext increments the current index and returns
                         // true if the new index is valid, or false if it's
                         // beyond the end of the list. If it returns true,
                         // it retrieves the value at that index and holds it 
                         // in an instance variable
    
        item = en.Current; // Current retrieves the value of the current instance
                           // variable
    

finally  

for:

int index = -1;
T item;

while(++index < test.Count)

    item = test[index];

如您所见,for 实现中的代码更少,foreachfor 之上有一个抽象层(枚举器)。我使用while 循环编写了for,以类似的表示形式显示两个版本。

说了这么多……

您说的是执行时间的微小差异。使用循环使代码更清晰更小,在这种情况下看起来像foreach

【讨论】:

您确定复制部分吗?我个人不熟悉 .NET,但我想不出任何其他面向对象的语言,foreach 会在其中创建每个对象实例的副本。典型的行为是只为下一个对象分配一个引用,而不是在内存中创建一个全新的实例。 @aroth:这将是值类型(这是 OP 所说的)和引用类型之间的区别。所有语言都在赋值时创建变量的的副本,区别在于值是引用类型是对实例的引用,值类型的值是结构本身. 很公平。对我来说,“复制”意味着在内存中创建一个新的独立对象实例,其状态与源实例相同。因此,引用(或其他任何东西)的赋值当然必然会创建引用的副本,因为您有两个具有相同值的变量,但内存中仍然只有一个实际对象的实例。但是,是的,structs 在示例代码中被分配,整个实例将在分配时被复制,就像你说的那样。【参考方案3】:

您没有在“for”测试后重置“秒表”,因此“for”测试所用的时间将被添加到后续的“foreach”测试中。此外,正如正确指定的那样,您应该在“for”内进行赋值以模仿 foreach 的确切行为。

sw.Start();

foreach (var item in test)




sw.Stop();
Console.WriteLine("foreach " + sw.ElapsedMilliseconds);
sw.Restart();

for (int j = 0; j < test.Count; j++)

    T temp = test[j];


sw.Stop();
Console.WriteLine("for " + sw.ElapsedMilliseconds);
sw.Reset(); // -- This bit is missing!

【讨论】:

【参考方案4】:

在您的for 循环中,我没有看到您实际访问来自test 的项目。如果将var x = test[i]; 添加到for 循环中,您会发现性能(实际上)是相同的。

对值类型属性的每次访问都会创建一个副本,使用 foreach 或在 for 循环中的 list 上使用 indexer

【讨论】:

【参考方案5】:

这里是关于Why should I use foreach instead of for (int i=0; i<length; i++) in loops?这个话题的讨论

【讨论】:

【参考方案6】:

我认为 foreach 提供了一种抽象的循环方式,但它在技术上比 for 循环慢,可以找到一篇关于 for 循环和 foreach 之间差异的好文章 here

【讨论】:

【参考方案7】:

你的测试不公平。考虑foreach 循环是如何运作的。你有以下代码:

foreach (var item in test)



这会创建一个变量item,并在每次迭代时从集合中获取下一个对象,并将其分配给item。此获取和分配不应创建副本,但访问基础集合并将正确的值分配给变量确实需要时间。

那么你有这个代码:

for (int j = 0; j < test.Count; j++)



这根本不访问底层集合。它不会在每次迭代时读取和分配变量。它只是增加一个整数test.Count 次,所以它当然更快。如果编译器很聪明,它会看到循环中没有任何操作发生,只是优化整个事情。

公平的比较会将第二段代码替换为:

var item;
for (int j = 0; j < test.Count; j++)

    item = test.get(j);
 

这与您的 foreach 循环所做的更具有可比性。

至于使用哪个,这真的是个人喜好和编码风格的问题。从可读性的角度来看,我一般觉得foreachfor(...) 更清晰。

【讨论】:

@CuiPengFei - 人们可能期望for 循环仍然更快一点,因为foreach 循环可能使用某种枚举接口/协议(即函数调用)来获取下一个对象实例。另一方面,修改后的for 循环只是直接访问下一个对象。由于函数调用是相对昂贵的操作,这可以解释为什么for 循环仍然更快。您可以通过执行static T getItem(int index) return test[index]; 之类的操作并从您的for 循环中调用它来进一步平衡它。 我刚试过。现在 for 循环运行速度较慢。谢谢。这解决了我的疑问。 您可能希望更正您的答案,因为提取和分配创建一个副本(如上所述)。此外,属性访问(通过列表中的索引器this[int index])和访问foreach 调用生成的枚举器的Current 属性之间没有区别,除了额外的间接级别。虽然这可能(并且可能确实)对性能产生一些影响,但您并没有将函数调用与“直接访问”进行比较。 @Cui:这是因为,除了不考虑IDisposable 枚举器并在try finally 内运行之外,使用GetEnumerator() 的代码与使用foreach 的代码相同。 此外,使用静态函数来检索列表项是不准确的,因为您现在将 另一个 变量(静态函数)引入等式。跨度> 【参考方案8】:

我只发现一个重要的案例 - 为 Windows Phone 7 开发。有两个原因需要改变

foreach(var item in colletion)


int length = array.Length;
for(int i = 0; i < length; ++i)


在 XNA 游戏中,如果集合很大或经常调用它(例如更新方法)。

有点快 垃圾少

而且垃圾很重要,因为 Compact Framework GC 会触发每 1MB 的分配,因此,它可能会导致烦人的冻结。

【讨论】:

以上是关于在 .NET 中,使用“foreach”迭代 IEnumerable<ValueType> 的实例会创建一个副本吗?那么我应该更喜欢使用“for”而不是“foreach”吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何在刀片foreach循环中获取迭代次数

在绑定调用中使用 SwiftUI ForEach 的迭代器变量

是否可以在 jstl 中使用 foreach 同时迭代两个项目?

如何在 ASP.NET Core 3.1 中逐行迭代列表

Java Iterables 使用每个 Foreach 构造“重置”迭代器

Mybatis 中 foreach 用法