在C#中,为什么不能在foreach循环中修改值类型实例的成员?


71

我知道值类型应该是不可变的,但这只是一个建议,而不是规则,对吗?那么为什么我不能做这样的事情:

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迭代变量”

这是为什么?


5
+1:好问题。我很久以前就知道foreach不能修改循环变量,但是我从来没有真正了解过为什么
jp2code 2011年

Answers:


77

因为foreach使用枚举器,并且枚举器不能更改基础集合,但是可以更改集合中对象引用的任何对象。这就是值和引用类型语义起作用的地方。

在引用类型(即类)上,集合所存储的所有内容都是对对象的引用。这样,它实际上从未接触过对象的任何成员,也不会对此有所不在意。对对象的更改不会影响集合。

另一方面,值类型将其整个结构存储在集合中。在不更改集合并使枚举器无效的情况下,您无法触摸其成员。

此外,枚举器返回集合中值的副本。在ref类型中,这没有任何意义。引用的副本将是相同的引用,并且您可以以所需的任何方式更改所引用的对象,且更改会超出范围。另一方面,在值类型上,意味着您得到的只是该对象的副本,因此对该副本所做的任何更改都将永远不会传播。


极好的答案。在哪里可以找到有关“值类型将其整个结构存储在集合中”的更多证据?
崔鹏飞崔鹏飞2011年

@CuiPengFei:值类型将其整个值存储在变量中。数组只是一个变量块。
亚当·罗宾逊

究竟。值类型会将其整个结构保存在所放置的任何容器中,无论是集合,本地var等。顺便说一下,这就是为什么最好使结构不可变的原因:您最终会获得大量“相同”的副本对象,很难跟踪是谁进行了更改,以及是否会在哪里传播。您知道,字符串发生的典型问题。
Kyte

非常简单的解释
3条规则

这种解释完全正确,但是我仍然想知道为什么枚举数以这种方式工作。无法实现枚举器,以使它们通过引用来操纵值类型的集合,从而使foreach能够有效运行,或者通过指定迭代变量为引用类型的选项来实现foreach来实现相同的目的。现有实施方式的局限性会带来任何实际好处吗?
Neutrino

16

从某种意义上说,这是一个建议,没有什么可以阻止您违反它,但是,与“这是一个建议”相比,它实际上应该有更多的分量。例如,由于您在这里看到的原因。

值类型将实际值存储在变量中,而不是引用中。这意味着您在数组中具有该值,并且在中具有该值的副本item,而不是引用。如果允许您更改中的值item,则它不会反映在数组中的值上,因为它是一个全新的值。这就是为什么不允许这样做的原因。

如果要执行此操作,则必须按索引遍历数组,而不能使用临时变量。



@史蒂文:我提到的原因是正确的,即使没有如链接答案中所述。
亚当·罗宾逊

您解释的所有内容都感觉正确,除了这句话“这就是为什么不允许这样做”之外。感觉不像是原因,我不确定,但这只是感觉不佳。而Kyte的答案更像是这样。
崔鹏飞崔鹏飞2011年

@CuiPengFei:这就是为什么不允许这样做的原因。不允许更改迭代器变量的实际,如果您在值类型上设置属性,则该操作是您要执行的操作。
亚当·罗宾逊

是的,我又读了一次。kyte的答案与您的答案几乎相同。谢谢
崔鹏飞崔鹏飞

10

结构是值类型。
类是引用类型。

ForEach构造使用IEnumerable数据类型元素的IEnumerator。发生这种情况时,就某种意义上讲,变量是只读的,即您无法修改它们,并且当您具有值类型时,就不能修改包含的任何值,因为它共享相同的内存。

C#语言规范第8.8.4节:

迭代变量对应于只读局部变量,其范围扩展到嵌入式语句中

修复由结构引起的此使用类;

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循环中调用它,它将很好地工作?
崔鹏飞崔鹏飞2011年

3

注意: 根据亚当的评论,这实际上不是问题的正确答案/原因。不过,这仍然值得牢记。

MSDN。使用枚举器时,您不能修改值,这实际上是foreach所做的。

枚举数可用于读取集合中的数据,但不能用于修改基础集合。

只要集合保持不变,枚举数将保持有效。如果对集合进行了更改(例如添加,修改或删除元素),则枚举数将无法恢复,并且其行为是不确定的。


1
虽然看起来像,但这不是问题所在。您当然可以修改通过其获得的实例的属性或字段值foreach(这不是MSDN文章所讨论的内容),因为这是在修改集合的成员,而不是集合本身。这里的问题是值类型与引用类型的语义。
亚当·罗宾逊

确实,我已经阅读了您的答案并以我的方式意识到了这个错误。我将保留它,因为它仍然很有用。
伊恩

谢谢,但是我不认为这是原因。因为我试图更改集合元素的成员之一,而不是集合本身。如果我将MyStruct从结构更改为类,则foreach循环将正常工作。
崔鹏飞崔鹏飞2011年

CuiPengFei-值类型与引用类型。从skeet的博客中了解有关它们的信息。
JonH 2011年



1

值类型由value调用。简而言之,当您评估变量时,将创建它的副本。即使允许这样做,您也将在编辑副本,而不是的原始实例MyStruct

foreach在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循环中调用它,它将可以正常工作。
崔鹏飞崔鹏飞2011年

@CuiPengFei:它可以编译,但是如果您随后验证原始实例,您会注意到它们没有改变。(由于值类型的“按值调用”行为。)
Steven Jeuris 2011年

哦,是的,对不起您的谨慎评论。
崔鹏飞崔鹏飞2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.