在 C# 中,为什么不能在 foreach 循环中修改值类型实例的成员?
我知道值类型应该是不可变的,但这只是一个建议,而不是规则,对吗? 那么为什么我不能这样做:
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 迭代变量”
为什么会这样?
I know that value types should be immutable, but that's just a suggestion, not a rule, right?
So why can't I do something like this:
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();
}
}
The foreach loop in the code doesn't compile while the commented for loop works fine. The error message:
Cannot modify members of 'item' because it is a 'foreach iteration variable'
Why is that?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
因为 foreach 使用枚举器,而枚举器无法更改底层集合,但是可以更改集合中对象引用的任何对象。这就是值和引用类型语义发挥作用的地方。
在引用类型(即类)上,集合存储的所有内容都是对对象的引用。因此,它实际上从未接触过对象的任何成员,也不会关心它们。对对象的更改不会影响集合。
另一方面,值类型将其整个结构存储在集合中。在不更改集合并使枚举器无效的情况下,您无法触摸其成员。
此外,枚举器返回集合中值的副本。在 ref 类型中,这没有任何意义。引用的副本将是相同的引用,并且您可以以任何您想要的方式更改引用的对象,并且更改会超出范围。另一方面,在值类型上,意味着您获得的只是对象的副本,因此该副本上的任何更改都永远不会传播。
Because foreach uses an enumerator, and enumerators can't change the underlying collection, but can, however, change any objects referenced by an object in the collection. This is where Value and Reference-type semantics come into play.
On a reference type, that is, a class, all the collection is storing is a reference to an object. As such, it never actually touches any of the object's members, and couldn't care less about them. A change to the object won't touch the collection.
On the other hand, value types store their entire structure in the collection. You can't touch its members without changing the collection and invalidating the enumerator.
Moreover, the enumerator returns a copy of the value in the collection. In a ref-type, this means nothing. A copy of a reference will be the same reference, and you can change the referenced object in any way you want with the changes spreading out of scope. On a value-type, on the other hand, means all you get is a copy of the object, and thus any changes on said copy will never propagate.
从某种意义上说,这是一个建议,没有什么可以阻止你违反它,但它确实应该比“这是一个建议”更重要。例如,出于您在这里看到的原因。
值类型将实际值存储在变量中,而不是引用。这意味着您的数组中有该值,并且在
item
中有该值的副本,而不是引用。如果允许您更改item
中的值,则它不会反映在数组中的值上,因为它是一个全新的值。这就是为什么它是不允许的。如果你想这样做,你必须按索引循环数组,而不是使用临时变量。
It's a suggestion in the sense that there's nothing stopping you from violating it, but it should really be afforded much more weight than "it's a suggestion". For instance, for the reasons you're seeing here.
Value types store the actual value in a variable, not a reference. That means that you have the value in your array, and you have a copy of that value in
item
, not a reference. If you were allowed to change the values initem
, it would not be reflected on the value in the array since it's a completely new value. This is why it isn't allowed.If you want to do this, you'll have to loop over the array by index and not use a temporary variable.
结构是值类型。
类是引用类型。
ForEach
构造使用 IEnumerable 数据类型元素的 IEnumerator。当发生这种情况时,变量在某种意义上是只读的,您无法修改它们,并且由于您具有值类型,因此您无法修改包含的任何值,因为它共享相同的内存。C# 语言规范第 8.8.4 节:
要修复此问题,请使用结构体中的类;
编辑:@CuiPengFei
如果您使用 var 隐式类型,编译器将很难帮助您。如果您使用
MyStruct
,它会告诉您结构的情况,它是只读的。对于类,对项目的引用是只读的,因此您不能在循环内编写item = null;
,但您可以更改其可变的属性。您还可以使用(如果您喜欢使用
struct
):Structures are value types.
Classes are reference types.
ForEach
construct uses IEnumerator of IEnumerable data type elements. When that happens, then variables are read-only in the sense, that you can not modify them, and as you have value type, then you can not modify any value contained, as it shares same memory.C# lang spec section 8.8.4 :
To fix this use class insted of structure;
Edit: @CuiPengFei
If you use
var
implicit type, it's harder for the compiler to help you. If you would useMyStruct
it would tell you in case of structure, that it is readonly. In case of classes the reference to the item is readonly, so you can not writeitem = null;
inside loop, but you can change it's properties, that are mutable.You can also use (if you like to use
struct
) :注意: 根据 Adam 的评论,这实际上并不是问题的正确答案/原因。不过,这仍然值得牢记。
来自 MSDN 。使用枚举器时无法修改值,这本质上就是 foreach 所做的事情。
NOTE: As per Adam's comment, this isn't actually the correct answer/cause of the problem. It's still something worth bearing in mind though.
From MSDN. You can't modify the values when using the Enumerator, which is essentially what foreach is doing.
我想你可以在下面找到答案。
C# 中的 Foreach 结构奇怪的编译错误
I think the you can find the anwser below.
Foreach struct weird compile error in C#
值类型按值调用。简而言之,当您评估变量时,会创建它的副本。即使允许这样做,您也将编辑副本,而不是
MyStruct
的原始实例。foreach
在 C# 中是只读的。对于引用类型(类),这不会有太大变化,因为只有引用是只读的,因此您仍然可以执行以下操作:但是,对于值类型(结构),整个对象是只读的,导致您遇到的情况。您无法调整 foreach 中的对象。
Value types are called by value. In short, when you evaluate the variable, a copy of it is made. Even if this was allowed, you would be editing a copy, rather than the original instance of
MyStruct
.foreach
is readonly in C#. For reference types (class) this doesn't change much, as only the reference is readonly, so you are still allowed to do the following:For value types however (struct), the entire object is readonly, resulting in what you are experiencing. You can't adjust the object in the foreach.
将 MyStruct 设为一个类(而不是结构),您就可以做到这一点。
Make MyStruct a class (instead of struct) and you'll be able to do that.
关于它的工作原理,所接受的答案是绝对正确的。但我认为不允许程序员做出这种改变有点过分了。
这样做很可能有充分的理由。
这是我的:
其中 OffsetParameters 是一个结构(值类型)
一切都很好 - 列表没有设置器,并且它的内容是值类型,因此它们受到消费者的保护。
所以现在我想向 MyClass 添加一个方法来更新该结构中的某些内容。这是我的第一个想法
实例化一个新列表,在旧列表上执行 foreach,在其中更新已经复制的迭代变量。将该副本添加到新列表中。
将 offsetList 分配给 foreach 之后的新列表。
看起来合法,但由于这个限制,我必须再制作该副本的另一个副本(效率低下)。
我知道这并不能回答问题,但它不适合对官方答案的评论,并且它为图片添加了重要的东西。
我的解决方案是在该列表上使用索引器而不是枚举器,仅供参考,以防您处于同一条船上:
我敢打赌,每次循环迭代都会重新计算 List.Count :) 如果您想要效率,请注意这一点
The accepted answer is absoluelty right regarding how it works. But I think not allowing a programmer to make that change is a bit of an overreach.
There may very well be a good reason to want to do this.
Here is mine:
where OffsetParameters is a struct (value type)
All is well - the list does not have a setter and the contents of it are of value type so they are protected from the consumer.
So now I want to add a method to MyClass to update something in that struct. Here is my first thought
instantiate a new list, do a foreach on the old list where I update the iteration var which is already copy. Add that copy to the new list.
Assign offsetList the new list after the for each.
Seems legit, but because of this restriction I have to make yet another copy of that copy (inefficient).
I know this does not answer the question, but it does not fit in a comment to the official answer and it adds smthng important to the picture.
My solution was to use indexer on that list instead of an enumerator just an FYI in case you are in the same boat:
I bet you List.Count gets recalculated every loop iteration :) watch out for that if you want efficiency