linq C# foreach是否从IEnumerable创建元素的副本?

hfyxw5xn  于 2023-06-19  发布在  C#
关注(0)|答案(3)|浏览(128)

在什么情况下foreach使用ref,在什么情况下foreach使用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
输出:

array.First = 999
enumerable.First = 0

似乎在foreach(var a in enumerable) {中a是copy而不是ref,而在foreach(var a in array) {中a是ref。
有人能解释一下吗?

fnvucqvd

fnvucqvd1#

这不是真的关于foreach。它与如何构造arrayenumerable有更多的关系。foreach在这两种情况下都做同样的事情(获取枚举器并调用MoveNextCurrent来迭代可枚举对象)。正是enumerablearray之间的差异导致了输出的差异。
array是一个A[],所以如果你改变它的元素v,然后得到第一个元素,你会明显地看到变化。A是引用类型,所以foreach中的a是对数组中元素的引用。
enumerable是由Select产生的。如果只是调用Select,则不会执行任何实质性操作。它只是创建了一个IEnumerable<A>,当上枚举时,创建了一堆A对象。这里重要的一点是,每当枚举enumerable时,都会运行Select lambda。如果你不列举它,什么也不会发生。这被称为deferred execution
因此,第二个foreach枚举enumerable,创建一堆A对象,然后更改这些Av。这是一个重要的区别--A对象不存储在任何地方,这与数组不同。在每次迭代之后,您都“丢弃”了A对象。
在最后调用enumerable.First()时,再次开始枚举enumerable--这次只枚举一次,因为只需要第一个元素。enumerable是什么?它通过运行Select中的代码创建一个新的A对象。

mitkmikd

mitkmikd2#

Enumerable.Select使用延迟执行,并返回一个在使用foreachGetEnumerator方法时“执行”的查询。.ToArray()返回包含对象的[]
第一个for循环遍历实际对象并修改元素的值,而第二个for循环投影序列中的每个元素。在enumerable上执行.First(),再次投影元素,并使用原始ints返回列表中的第一个元素

vbkedwbf

vbkedwbf3#

正如其他人所说,这是延迟执行与非延迟执行的一个示例。
举个例子:

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);
        }
    }
}

当我们执行foreach时,Select中的代码将为a中的每个项目运行(即,我们将枚举a,并在每次抓取a中的项目时运行控制台writeline和f + x)。
一旦我们添加了ToArray(),求值就必须立即发生,这意味着我们不再延迟执行。
在您的示例中,由于我们立即调用ToArray(),这意味着我们立即获得一堆对A对象的引用。此外,由于求值发生在该行上,因此我们有一个存储它们的位置。
对于enumerable变量,new A()的求值导致我们每次在foreach中迭代时都创建一个A对象,但我们刚刚创建的A对象不是原始enumerable集合的一部分,因此它丢失了。
如果您采用我的示例,并在Select()行上调用ToArray()和不调用ToArray()的情况下运行它,您会看得更清楚。

相关问题