在 C# 中,为啥我不能在 foreach 循环中修改值类型实例的成员?
Posted
技术标签:
【中文标题】在 C# 中,为啥我不能在 foreach 循环中修改值类型实例的成员?【英文标题】:In C#, why can't I modify the member of a value type instance in a foreach loop?在 C# 中,为什么我不能在 foreach 循环中修改值类型实例的成员? 【发布时间】:2011-08-04 18:36:43 【问题描述】:我知道值类型应该是不可变的,但这只是一个建议,而不是规则,对吧? 那么为什么我不能这样做:
struct MyStruct
public string Name get; set;
public class Program
static void Main(string[] args)
MyStruct[] array = new MyStruct[] new MyStruct Name = "1" , new MyStruct Name = "2" ;
foreach (var item in array)
item.Name = "3";
//for (int i = 0; i < array.Length; i++)
//
// array[i].Name = "3";
//
Console.ReadLine();
代码中的 foreach 循环无法编译,而注释的 for 循环工作正常。错误信息:
不能修改“item”的成员,因为它是一个“foreach 迭代变量”
这是为什么呢?
【问题讨论】:
+1:好问题。我早就知道foreach
循环变量无法修改,但我从未真正了解为什么!
【参考方案1】:
因为 foreach 使用枚举器,并且枚举器不能更改底层集合,但是 可以 更改集合中的对象引用的任何对象。这就是值和引用类型语义发挥作用的地方。
在一个引用类型,即一个类上,所有的集合存储的是一个对象的引用。因此,它从未真正接触过该对象的任何成员,也不会不在乎它们。对对象的更改不会触及集合。
另一方面,值类型将其整个结构存储在集合中。如果不更改集合并使枚举器无效,则无法触摸其成员。
此外,枚举器返回集合中值的副本。在 ref 类型中,这没有任何意义。引用的副本将是相同的引用,并且您可以以任何您想要的方式更改引用的对象,而更改超出范围。另一方面,值类型意味着您得到的只是对象的副本,因此对所述副本的任何更改都不会传播。
【讨论】:
优秀的答案。在哪里可以找到更多关于“值类型将其整个结构存储在集合中”的证据? @CuiPengFei:值类型将它们的整个值存储在一个变量中。数组只是一个变量块。 没错。值类型将在其放置的任何容器中保存其整个结构,无论是集合、本地变量等。顺便说一下,这就是为什么最好使结构不可变的原因:您最终会得到大量“相同”的副本对象,并且很难跟踪哪个人进行了更改以及它是否会传播以及传播到哪里。你知道,字符串发生的典型问题。 很简单的解释 这个解释完全正确,但我仍然想知道 为什么 枚举器以这种方式工作。不能实现枚举器,以便它们通过引用操作值类型的集合,从而使 foreach 能够有效地运行,或者使用指定迭代变量为引用类型的选项来实现 foreach 以实现相同的目的。现有实施的局限性是否带来任何实际好处?【参考方案2】:从某种意义上说,这是一个建议,没有什么可以阻止你违反它,但它确实应该比“这是一个建议”更重要。例如,出于您在此处看到的原因。
值类型将实际值存储在变量中,而不是引用中。这意味着您的数组中有值,并且在item
中有该值的副本,而不是引用。如果您被允许更改item
中的值,它不会反映在数组中的值上,因为它是一个全新的值。这就是不允许的原因。
如果你想这样做,你必须按索引循环数组,而不是使用临时变量。
【讨论】:
我没有投反对票,但我相信the reason you mention why foreach is readonly isn't entirely correct。 @Steven:我提到的原因是正确的,如果没有像链接答案中那样彻底描述的话。 你解释的一切感觉都对,除了这句话“这就是不允许的原因”。感觉不是原因,我不确定,但就是感觉不像。而Kyte的回答更像。 @CuiPengFei:这就是不允许的原因。您不能更改迭代器变量的实际 值,如果您在值类型上设置属性,您就是这样做的。 是的,我刚刚又读了一遍。 kyte 的答案和你的几乎一样。谢谢【参考方案3】:结构是值类型。 类是引用类型。
ForEach
构造使用 IEnumerable 数据类型元素的 IEnumerator。当这种情况发生时,变量在某种意义上是只读的,你不能修改它们,并且由于你有值类型,所以你不能修改包含的任何值,因为它共享相同的内存。
C# 语言规范第 8.8.4 节:
迭代变量对应一个只读局部变量,其作用域延伸到嵌入语句上
修复这个使用类 insted 的结构;
class MyStruct
public string Name get; set;
编辑:@崔鹏飞
如果你使用var
隐式类型,编译器就更难帮你了。如果您使用MyStruct
,它会在结构的情况下告诉您,它是只读的。在类的情况下,对该项目的引用是只读的,因此您不能在循环内写入item = null;
,但您可以更改它的属性,即可变的。
你也可以使用(如果你喜欢使用struct
):
MyStruct[] array = new MyStruct[] new MyStruct Name = "1" , new MyStruct Name = "2" ;
for (int index=0; index < array.Length; index++)
var item = array[index];
item.Name = "3";
【讨论】:
谢谢,但是您如何解释这样一个事实,即如果我将 ChangeName 方法添加到 MyStruct 并在 foreach 循环中调用它,它会正常工作?【参考方案4】:注意: 根据亚当的评论,这实际上不是问题的正确答案/原因。不过,这仍然值得牢记。
来自MSDN。使用枚举器时不能修改值,这实际上是 foreach 正在做的事情。
枚举器可用于读取 集合中的数据,但它们 不能用于修改 基础集合。
枚举数保持有效,只要 集合保持不变。如果 对集合进行了更改, 例如添加、修改或删除 元素,枚举数是 不可恢复的无效及其 行为未定义。
【讨论】:
虽然看起来像,但这不是问题中的问题。您当然可以修改通过foreach
获得的实例的属性或字段值(这不是 MSDN 文章所说的),因为那是在修改集合的 member,而不是集合本身。这里的问题是值类型与引用类型语义。
确实,我刚刚阅读了您的回答,并以我的方式意识到了错误。我会留下这个,因为它仍然有点用。
谢谢,但我不认为这是原因。因为我正在尝试更改集合元素的成员之一,而不是集合本身。如果我将 MyStruct 从结构更改为类,则 foreach 循环将正常工作。
CuiPengFei - 值类型与引用类型。从 skeet 的博客中了解它们。【参考方案5】:
使 MyStruct 成为一个类(而不是结构),您就可以做到这一点。
【讨论】:
这是正确的基于 VT(值类型)与 RT(参考类型),但并没有真正回答问题。 谢谢,我知道那会成功的。但我只是想找出原因。 @CuiPengFei:我的回答解释了为什么会这样。【参考方案6】:我想你可以在下面找到答案。
Foreach struct weird compile error in C#
【讨论】:
【参考方案7】:值类型为called by value。简而言之,当您评估变量时,会制作它的副本。即使允许这样做,您也将编辑一个副本,而不是 MyStruct
的原始实例。
foreach
is readonly in C#。对于引用类型(类),这并没有太大变化,因为只有引用是只读的,所以您仍然可以执行以下操作:
MyClass[] array = new MyClass[]
new MyClass Name = "1" ,
new MyClass Name = "2"
;
foreach ( var item in array )
item.Name = "3";
然而,对于值类型(结构),整个对象是只读的,导致您正在经历的事情。您无法在 foreach 中调整对象。
【讨论】:
谢谢,但我不认为你的解释是 100% 正确的。因为如果我将 ChangeName 方法添加到 MyStruct 并在 foreach 循环中调用它,它会工作得很好。 @CuiPengFei:它可以编译,但是如果你之后验证原始实例,你会发现它们没有改变。 (由于值类型的“按值调用”行为。) 哦,是的,对于不谨慎的评论感到抱歉。以上是关于在 C# 中,为啥我不能在 foreach 循环中修改值类型实例的成员?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我不能访问这个数组 ForEach 循环中的数据? SwiftUI