在 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 循环中设置迭代变量的属性?

为啥我不能访问这个数组 ForEach 循环中的数据? SwiftUI

为啥在foreach循环中不能修改值类型实例

为啥我不能使用 foreach 循环更新数组中的数据?

为啥我应该在循环中使用 foreach 而不是 for (int i=0; i<length; i++) ?

为啥有些 Enumerable 可以在 foreach 中更改,而有些则不能?