[英]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
构造使用IEnumerable的IEnumerable数据类型元素。 当发生这种情况时,变量在某种意义上是只读的,您无法修改它们,并且由于您具有值类型,因此您无法修改包含的任何值,因为它共享相同的内存。
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。
值类型按值调用 。 简而言之,当您评估变量时,会对其进行复制。 即使允许这样做,您也可以编辑副本,而不是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.