繁体   English   中英

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

[英]In C#, why can't I modify the member of a value type instance in a foreach loop?

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

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

这是为什么?

因为foreach使用枚举器,并且枚举器不能更改底层集合,但是可以更改集合中对象引用的任何对象。 这就是Value和Reference-type语义发挥作用的地方。

在引用类型(即类)上,存储的所有集合都是对对象的引用。 因此,它实际上从未触及任何对象的成员,并且对它们的关注度不高。 对象的更改不会触及该集合。

另一方面,值类型将其整个结构存储在集合中。 如果不更改集合并使枚举器无效,则无法触及其成员。

此外,枚举器返回集合中值的副本 在ref-type中,这意味着什么。 引用的副本将是相同的引用,您可以以任何方式更改引用的对象,并将更改扩展到范围之外。 另一方面,在值类型上,意味着您获得的只是对象的副本,因此对所述副本的任何更改都不会传播。

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

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

如果你想这样做,你将不得不通过索引循环数组而不是使用临时变量。

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

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

C#lang规范部分8.8.4:

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

修复这个使用类的结构;

class MyStruct {
    public string Name { get; set; }
}

编辑:@CuiPengFei

如果使用var implicit类型,编译器很难帮助你。 如果您使用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";
        }

注意: 根据Adam的评论,这实际上不是问题的正确答案/原因。 尽管如此,它仍然值得记住。

来自MSDN 使用Enumerator时无法修改值,这实际上是foreach正在执行的操作。

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

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

使MyStruct成为一个类(而不是struct),你就能做到这一点。

我想你可以在下面找到anwser。

在C#中Foreach结构奇怪的编译错误

值类型按值调用 简而言之,当您评估变量时,会对其进行复制。 即使允许这样做,您也可以编辑副本,而不是MyStruct的原始实例。

foreach只在C#中读取 对于引用类型(类),这并没有太大变化,因为只有引用是只读的,所以仍然允许您执行以下操作:

    MyClass[] array = new MyClass[] {
        new MyClass { Name = "1" },
        new MyClass { Name = "2" }
    };
    foreach ( var item in array )
    {
        item.Name = "3";
    }

但是对于值类型(struct),整个对象是只读的,从而导致您遇到的问题。 您无法在foreach中调整对象。

公认的答案是关于它如何工作的绝对正确的。 但我认为不允许程序员做出这种改变有点过头了。 很可能有充分的理由想要这样做。 这是我的:

     class MYClass
     {
        private List<OffsetParameters> offsetList = new List<OffsetParameters>();

        internal List<OffsetParameters> OffsetParamList
        {
            get { return this.offsetList; }
        }

其中 OffsetParameters 是一个结构(值类型)

    internal struct OffsetParameters
    {
    ...

一切都很好 - 该列表没有设置器,并且它的内容是值类型,因此它们受到消费者的保护。 所以现在我想向 MyClass 添加一个方法来更新该结构中的某些内容。 这是我的第一个想法是实例化一个新列表,在旧列表上执行 foreach 更新已经复制的迭代 var。 将该副本添加到新列表中。 在 for each 之后分配 offsetList 新列表。

        internal void UpdateColors(Dictionary<string, string> nameColorMap)
        {
            if (nameColorMap == null)
                return; 

            List<OffsetParameters> newOffsetParamList = new List<OffsetParameters>();
            foreach (OffsetParameters param in offsetList)
            {
                if (nameColorMap.ContainsKey(param.offsetName))
                    param.color = nameColorMap[param.offsetName];
                newOffsetParamList.Add(param);
            }
            offsetList = newOffsetParamList;
        }

似乎是合法的,但由于这个限制,我必须制作该副本的另一个副本(效率低下)。 我知道这不能回答问题,但它不适合对官方答案的评论,它增加了对图片的重要性。

我的解决方案是在该列表上使用索引器而不是枚举器,只是一个仅供参考,以防你在同一条船上:

        internal void ChangeOffsetGroupColors(Dictionary<string, string> nameColorMap)
        {
            if (nameColorMap == null)
                return; 

            List<OffsetParameters> newOffsetParamList = new List<OffsetParameters>();
            for (int i = 0; i < offsetList.Count; i++)
            {
                OffsetParameters param = offsetList[i];
                if (nameColorMap.ContainsKey(offsetList[i].offsetName))
                    param.color = nameColorMap[offsetList[i].offsetName];
                newOffsetParamList.Add(param);
            }
            offsetList = newOffsetParamList;
        }

我敢打赌,每次循环迭代都会重新计算 List.Count :) 如果你想提高效率,请注意这一点

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM