C# foreach是否在IEnumerable中创建元素的副本?

huangapple go评论65阅读模式
英文:

Does C# foreach create a copy of the element from an IEnumerable?

问题

在哪些情况下 foreach 使用引用,以及在哪些情况下 foreach 使用副本?

    class A {
        public int v;
    }
    
    class Program
    {
        static void Main() {
            var ints = new int[] { 0, 1, 2 };
            
            var array = ints.Select(i=>new A {v = i}).ToArray();
            foreach(var a in array) {
                a.v = 999;
            }
    
            var enumerable = ints.Select(i=>new A {v = i});
            foreach(var a in enumerable) {
                a.v = 999;
            }
    
            Console.WriteLine($"array.First = {array.First().v}");
            Console.WriteLine($"enumerable.First = {enumerable.First().v}");
        }
    }

输出:

    array.First = 999
    enumerable.First = 0

似乎在 foreach(var a in enumerable) { 中,a 是副本而不是引用;而在 foreach(var a in array) { 中,a 是引用。

有人能解释一下这个吗?

英文:

In what cases foreach uses a ref, and in what cases foreach uses a copy?

using System;
using System.Linq;

class A {
    public int v;
}

class Program
{
    static void Main() {
        var ints = new int[] { 0, 1, 2 };
        
        var array = ints.Select(i=>new A {v = i}).ToArray();
        foreach(var a in array) {
            a.v = 999;
        }

        var enumerable = ints.Select(i=>new A {v = i});
        foreach(var a in enumerable) {
            a.v = 999;
        }

        Console.WriteLine($"array.First = {array.First().v}");
        Console.WriteLine($"enumerable.First = {enumerable.First().v}");
    }
}

jdoodle.com/ia/Jce

Output:

array.First = 999
enumerable.First = 0

Seems that in foreach(var a in enumerable) { a is copy not a ref, while in foreach(var a in array) { a is a ref.

Can someone explain this?

答案1

得分: 5

这实际上与foreach无关。它更多地与您如何构造arrayenumerable有关。在这两种情况下,foreach执行相同的操作(获取枚举器并调用MoveNextCurrent以遍历可枚举对象)。导致输出差异的是enumerablearray之间的区别。

array是一个A[],因此如果您更改其元素的v,然后获取其中一个元素的第一个元素,显然会看到更改。A是引用类型,因此foreach中的a是对数组中元素的引用。

然而,enumerable是由Select生成的东西。如果只调用Select,则不会执行任何实质性操作。它只会创建一个IEnumerable<A>,当对其进行枚举时,它会创建一堆A对象。这里的重要点在于,只有在枚举enumerable时才会运行Select lambda。如果您不枚举它,什么都不会发生。这被称为延迟执行

因此,第二个foreach枚举enumerable,创建了一堆A对象,然后更改了这些Av。这里的重要区别在于 - A对象不会被存储在任何地方,不像数组。在每次迭代后,您都"丢弃"了A对象。

当您在最后调用enumerable.First()时,您再次开始枚举enumerable - 这次只有一次,因为您只想要第一个元素。enumerable会做什么呢?它通过运行Select中的代码创建一个新的A对象。

英文:

This is not really about foreach. It has more to do with how you constructed array and enumerable. foreach does the same thing in both cases (getting the enumerator and calling MoveNext and Current to iterate through the enumerable). It is the difference between enumerable and array that causes the difference in output.

array is an A[], so if you change the vs of its elements, and then get the first of those element, you will obviously see the change. A is a reference type, so a in the foreach is a reference to the element in the array.

enumerable however, is something produced by Select. Nothing substantial is done if you just call Select. It just creates an IEnumerable<A>, that when enumerated over, creates a bunch of A objects. The important point here is that the Select lambda is run whenever enumerable is being enumerated. If you don't enumerate it, nothing happens. This is called deferred execution.

So the second foreach enumerates enumerable, creating a bunch of A objects, and you then change the vs of those As. Here is the important difference - the A objects don't get stored anywhere, unlike with an array. You "threw" away the A object after each iteration.

When you call enumerable.First() at the end, you start enumerating enumerable again - only once this time because you want only the first element. And what does enumerable do? It creates a new A object by running the code in Select.

答案2

得分: 1

Enumerable.Select使用延迟执行,并返回一个在使用foreachGetEnumerator方法时被"执行"的查询。.ToArray()返回包含对象的[]

第一个for循环迭代实际对象并修改元素的值,而第二个for循环则对序列的每个元素进行投影。在enumerable上执行.First()会再次对元素进行投影,并使用原始的ints返回列表中的第一个元素。

英文:

Enumerable.Select uses deferred execution and returns a query that is "executed" while using the foreach or GetEnumerator method. .ToArray() returns an [] that contains the objects.

The first for loop iterates over the actual objects and modifies the values of the elements, while the second for loop projects each element of the sequence. Executing .First() on enumerable, projects the elements again and uses the original ints to return the first element from the list

答案3

得分: 1

以下是翻译好的部分:

正如其他人所说,这是延迟执行与非延迟执行的实例。

以以下示例为例:

internal class Program
{
    // 注意这在 Main 中的设置位置!
    static int x = 0;

    private static void Main(string[] args)
    {
        int[] arr = { 1, 2, 3 };
        var a = arr.Select(f => {
            Console.WriteLine("已评估");
            return f + x;
        });

        // 这会影响上面的 Select 如何评估事物。
        x = 5;

        foreach (var d in a)
        {
            Console.WriteLine(d);
        }
    }
}

当我们执行 foreach 时,Select 中的代码将针对 a 中的每个项目运行(即,我们将枚举 a 并在每次获取 a 中的项目时运行控制台写入和 f + x)。

一旦我们添加 ToArray(),评估必须立即发生,这意味着我们不再延迟执行。

在你的示例中,由于我们立即调用 ToArray(),这意味着我们立即获得了许多对 A 对象的引用。此外,由于评估发生在该行上,我们有一个存储它们的地方。

对于你的 enumerable 变量,new A() 的评估意味着我们在 foreach 中每次迭代时都创建一个 A 对象,但我们刚刚创建的 A 对象不是原始的 enumerable 集合的一部分,因此它会丢失。

如果你采用我的示例并在具有 Select() 的行上运行它,带有和不带有对 ToArray() 的调用,你会更清楚地看到这些情况。

英文:

As others have said, this is an instance of deferred execution vs non-deferred execution.

Take the following example:

internal class Program
{
    // Note where this gets set in Main!
    static int x = 0;

    private static void Main(string[] args)
    {
        int[] arr = { 1, 2, 3 };
        var a = arr.Select(f => {
            Console.WriteLine("Evaluated");
            return f + x;
        });

        // This will affect how the above Select evaluates things.
        x = 5;

        foreach (var d in a)
        {
            Console.WriteLine(d);
        }
    }
}

When we do our foreach, the code in the Select will be run for each item in a (i.e., we will enumerate over a and run the console writeline and f + x each time we grab an item in a).

As soon as we tack on ToArray(), the evaluation has to happen immediately, meaning we no longer defer execution.

In your example, since we call ToArray() immediately, it means we immediately get a bunch of references to A objects. Additionally, since the evaluation happens on that line, we have a place that we're storing them.

With your enumerable variable, the evaluation of new A() results in us creating an A object each time we iterate in the foreach, but the A object we just made isn't apart of the original enumerable collection, so it's lost.

You'll see things more clearly if you take my example and run it with and without a call to ToArray() on the line with Select().

huangapple
  • 本文由 发表于 2023年6月15日 10:07:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/76478636.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定