foreach()循环是否通过引用迭代?

83

考虑以下内容:

List<MyClass> obj_list = get_the_list();
foreach( MyClass obj in obj_list )
{
    obj.property = 42;
}

obj 是指向列表中相应对象的引用,因此当我更改属性时,更改将保留在某个地方构造的对象实例中吗?


相关问题:https://dev59.com/pXI-5IYBdhLWcg3w6tFR - AJ.
自C#7.3以来,有foreach(ref var x in XXX)语法可用,当EnumeratorCurrent属性返回ref T(例如在Span<T>上进行迭代)时。有关详细信息,请参见@Wolf的答案。 - R2RT
10个回答

80

是的,obj是集合中当前对象的引用(假设MyClass确实是一个类)。如果您通过引用更改任何属性,则会更改对象,就像您预期的那样。

但请注意,无法更改变量obj本身,因为它是迭代变量。如果尝试这样做,将会得到编译错误。这意味着您无法将其设置为空,并且如果您正在迭代值类型,则不能修改任何成员,因为那将更改该值。

C#语言规范说明(8.8.4)

"迭代变量对应于范围延伸到嵌入语句的只读局部变量。"


1
即使对于值类型,例如结构体,这也是正确的。 - technophile
7
如果MyClass是一个结构体,实际上你不能修改obj,因为你不被允许修改迭代变量本身。 - Brian Rasmussen
2
根据语言规范(8.8.4),“迭代变量对应于一个只读本地变量,其作用域延伸到嵌入语句。” @Amy - Brian Rasmussen
1
@Amy:是的,但你没有修改 obj 本身,而是修改了它指向的东西。这是有区别的。试着将 obj 设置为 null,你会得到编译错误。 - Brian Rasmussen
1
@Amy:不完全是这样。值类型变量存储实际值,引用类型变量存储对象的引用。变量而非所引用的对象是只读的,因此您可以通过引用更改对象,但无法更改引用本身。 - Brian Rasmussen
显示剩余6条评论

23
是的,直到你将泛型类型从 List 更改为 IEnumerable。

8
有人可以详细说明一下吗? - Justin Obney
3
我需要进一步解释为什么会出现这种情况。 - Sarah Bailey
2
我认为这可能与此有关:https://dev59.com/313Ua4cB1Zd3GeqP8w3A#8064426 - MatrixManAtYrService
@MatrixManAtYrService 很有可能就是参考这个例子,其中 IEnumerable 作用于新构造的对象。据我所知,使用 IEnumerable 和 List 没有区别。 - sean
这是纯属虚假和误导性的。 - Orestis P.

22

您在这里提出了两个不同的问题,我们按顺序来回答。

foreach循环是否按引用迭代?

如果您的意思是与C++中的for循环类似的方式,请注意,不是的。C#没有像C++那样的局部变量引用,因此不支持这种类型的迭代。

更改是否会持久化

假设MyClass是引用类型,那么答案是肯定的。在.NET中,类是引用类型,因此迭代变量是一个对一个变量的引用,而不是副本。但对于值类型则不成立。


17

我的情况是,在使用 foreach 循环遍历 var 集合时,我的更改没有被更新:

var players = this.GetAllPlayers();
foreach (Player player in players)
{
    player.Position = 1;
}

当我将 var 改为 List 时它开始工作了。


1
很有趣。我想知道在这种情况下是否克隆了集合。 - sharkin
使用foreach时,有两种情况。1.如果您的类实现了IEnumerable/IEnumerable<T>,它可以强制转换为该接口。如果您的集合是一个结构体,则会被装箱。2.直接使用GetEnumerator()方法。这是一种方法模式。它允许在值类型上使用基本枚举器而不需要昂贵的装箱。IEnumerable根本不是必需的。但是,当然没有linq(我们需要traits或某种值类型接口)。 - Arek Bal

12
在这种情况下(使用 List<T>),您可以这样做,但如果您要迭代通用的 IEnumerable<T>,那么它将取决于其实现。
如果它仍然是 List<T>T[],则所有内容都将按预期工作。当您使用 yield 构造一个 IEnumerable<T> 时,最大的问题就来了。在这种情况下,您不能再在迭代中修改 T 的属性,并期望在再次迭代相同的 IEnumerable<T> 时呈现。

9
也许你会感兴趣的是,通过C# 7.3版本,可以在枚举器的“Current”属性返回引用类型时,通过引用来修改值。以下内容来自微软官方文档(直接复制):
Span<int> storage = stackalloc int[10];
int num = 0;
foreach (ref int item in storage)
{
    item = num++;
}

您可以在C# foreach语句 | Microsoft Docs中了解更多关于这个新功能的内容。


4
只要不是一个结构体,这就是真的。

1
哦,好的知道了!读了最佳答案后,我一直在想为什么我的代码不起作用,但这解释了一切。将结构体改为类,* 魔法 *它就可以工作了。 - Luc

2
好的,不了解您所说的“迭代引用”的具体含义,我无法具体回答是或否,但我可以说,在表面下发生的事情是,.net框架为每次客户端代码调用foreach构造一个“枚举器”类,在整个循环期间维护对正在迭代的集合的引用指针,每次foreach迭代时,将“传递”一个项目并“增加”枚举器中的指针或引用到下一个项目...

这种情况发生在您迭代的集合中的项目是值类型还是引用类型都一样。


0

obj是List中的一个项目的引用,因此如果您更改它的值,它将持久存在。现在您应该关注的是get_the_list();是否正在对List进行深度复制或返回相同的实例。


2
它对他感兴趣的功能没有影响。四个相同列表的副本将都引用相同的实际对象,因此无论您是从List1还是List3修改它们,都会导致修改原始对象。唯一会受到影响的是如果您添加或删除列表中的项目,则不会影响列表的任何副本。 - technophile
是的,简短的回答当然是"是的,它是一个引用类型",但他可能会遇到问题,如果他创建了一份原始列表的副本,因此如果他调用 get_the_list(); 改变它,然后在代码的其他地方再次调用 get_the_list(); ,他可能得不到预期的结果。 - Stan R.

0

没错,这也是为什么你不能在 foreach 语句的上下文中更改可枚举对象的原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接