为什么在foreach循环中赋值运算符(=)无效?

30

为什么在foreach循环中赋值运算符(=)无效?我正在使用C#,但我认为对于其他支持foreach的语言来说,这个问题也是相同的(例如PHP)。例如,如果我像下面这样做:

string[] sArray = new string[5];

foreach (string item in sArray)
{
   item = "Some assignment.\r\n";
}

我遇到了一个错误:"无法向 'item' 赋值,因为它是一个 'foreach 迭代变量'。"


8
一般而言,PHP 可以让你做一些使软件维护变得困难的事情。更重要的是,它与大多数其他编程语言并不相似。在其他编程语言中使用 PHP 成语可能不是一个好主意。请注意保持翻译内容的准确性和原意。 - Billy ONeal
重复问题:https://dev59.com/M3RA5IYBdhLWcg3w8SiJ - chilltemp
10个回答

70

以下是您的代码:

foreach (string item in sArray)
{
   item = "Some assignment.\r\n";
}

以下是编译器对此所做的大致近似:

using (var enumerator = sArray.GetEnumerator())
{
    string item;
    while (enumerator.MoveNext())
    {
        item = enumerator.Current;

        // Your code gets put here
    }
}
IEnumerator<T>.Current属性是只读的,但实际上在这里并不相关,因为您正在尝试将局部变量item赋值为一个新值。编译时检查防止您这样做基本上是为了保护您免受执行不像您期望的操作(例如更改局部变量而对基础集合/序列没有影响)的影响。
如果您想在枚举时修改类似于string[]这样的索引集合的内部,请使用传统的for循环而不是foreach
for (int i = 0; i < sArray.Length; ++i)
{
    sArray[i] = "Some assignment.\r\n";
}

2
很棒的答案!终于有一个恰当地解释了正在发生的事情。 - Kirk Woll
我永远不会理解编译器:/ - BoltClock
2
虽然你说 foreach 只是语法糖没错,但是你对编译器实际转换的理解有点不准确。在 Eric Lippert 的博客 中有一个更准确的解释。这篇文章讨论了闭包,但也解释了编译器的工作原理。其中一个非常重要的区别是临时变量在循环外部声明。 - Roman
@R0MANARMY:感谢您指出这一点;显然,在大多数情况下,这两个结构并没有太大的区别,但我已经改变了我的答案,以免在那些它们不同的情况下误导读者。尽管如此,我仍然选择使用using而不是try/finally块,因为毕竟编译器会将using转换为后者(我的主要关注点是可读性)。 - Dan Tao
你忘记释放枚举器了。根据枚举器的实现方式,这可能会导致严重的内存泄漏。 - supercat
1
@supercat:我没忘!这就是 using 语句所做的。 - Dan Tao

6
因为语言规范如此规定。但是说真的,并不是所有的序列都是数组或可逻辑修改或写入的东西。例如:
foreach (var i in Enumerable.Range(1, 100)) {
   // modification of `i` will not make much sense here.
}

尽管在技术上可以让 i = something; 修改一个本地变量,但这可能会产生误导(你可能会认为它真的在幕后更改了某些东西,但实际情况并非如此)。
为了支持这些类型的序列,IEnumerable<T> 不需要 set 访问器来修改其 Current 属性,使其成为只读属性。 因此,foreach 无法使用 Current 属性修改底层集合(如果存在)。

这个和yield关键字是我首先想到的,它们是真正需要被保护的事情。虽然C#很少能保护我们免受"意外行为"的侵害,但你的例子值得我们加以保护。 - Jimmy Hoffa

5
foreach循环是设计用于遍历集合中的对象,而不是为了分配事物-这只是语言的设计。
此外,来自MSDN的说明:
“当在只读上下文中对变量进行赋值时,将发生此错误。只读上下文包括foreach迭代变量、using变量和fixed变量。要解决此错误,请避免在使用块、foreach语句和fixed语句中对语句变量进行赋值。” foreach关键字只枚举IEnumerable实例(通过调用GetEnumerator()方法获取IEnumerator实例)。 IEnumerator是只读的,因此值不能在foreach上下文中更改。

4
因为你不能使用 foreach 循环来修改你正在遍历的数组。循环通过数组进行迭代,因此如果尝试修改正在迭代的内容,则可能会发生意外的行为。此外,正如 Darin 和 DMan 指出的那样,您正在迭代一个只读的 IEnumerable
PHP 在其 foreach 循环中复制了数组并遍历该副本,除非你使用引用,否则你将修改数组自身。

1
好的,只要您不对其进行赋值,就可以修改它。例如,如果您正在“foreach”一堆对象,您可以更改对象的内部,但实际上不能对对象本身进行赋值。 - Robaticus

2

通常情况下,如果您正在尝试这样做,您需要仔细考虑您的设计,因为您可能没有使用最佳构造。在这种情况下,最好的答案可能是

string[] sArray = Enumerable.Repeat("Some assignment.\r\n", 5).ToArray();

在C#中,几乎总是可以使用更高级别的结构代替这种循环。(而在C++中则是另一个话题)


1
字符串不是值类型。更重要的是,将局部变量分配给新值永远不会改变其他地方的引用值,除非它是一个ref参数(在这种情况下,它实际上是相同的引用)。也许你应该说,“因为item具有局部作用域…”? - Dan Tao
@Dan Tao:我已经删除了我的回答中的那一部分;其他回答在解释我的意思方面做得更好。 - Billy ONeal
我想你只是打错了。每个人都总是急于第一个得到答案 - 我知道我也是这样的 ;) - Dan Tao
我喜欢这个答案!!!它比for循环要整洁得多。我知道这不是很常见,但有时(在测试用例中),你需要生成一个100个元素的数组。 - John Henckel

2

因为 IEnumerable 是只读的。


1
我不确定这真正解释了为什么“当前项目变量”是只读的。 - Kirk Woll
1
@Kirk:因为从“IEnumerable”对象读取时,当前项目不会被复制。 - BoltClock
@Kirk Woll,您不能使用IEnumerator修改值。 IEnumerator.Current属性是只读的,它没有setter。 - Darin Dimitrov
@Darin:我没有点踩,但 Kirk Woll 的评论可能是原因的根源。 - Billy ONeal
@Darin:我也没有给你的回答点踩,但我认为你回答有误导性,因为你混淆了局部变量和Current属性。请参考Mehrdad或我的回答来理解我的意思。 - Dan Tao

1

你不能在 foreach 循环中修改正在遍历的数组。请使用以下代码:

string[] sArray = new string[5]; 

for (int i=0;i<sArray.Length;i++)
{
    item[i] = "Some Assignment.\r\n";
}

1
你可以使用 Enumerable.Repeat 将整个事情简化为一个语句。 - Billy ONeal

0
foreach 循环被设计用于遍历数组一次,不会重复或跳过(虽然您可以通过使用 continue 关键字在 foreach 结构中跳过某些操作)。 如果您想修改当前项,请考虑使用 for 循环。

0

在“ForEach”循环遍历列表时,您无法修改该列表。

最好的选择是简单地创建一个临时列表来存储您希望使用的项目。


不,最好的选择是使用现成的组件,让你避免首先使用循环,或者使用下标而不是枚举器编写循环。 - Billy ONeal

0

让它可以被修改完全是可能的。然而,这意味着什么?它会读起来像底层枚举已被修改,但实际上并没有被修改(虽然也有可能允许这样做,但这有其自身的缺点)。

因此,你会得到一个人们自然而然会读错所发生的事情的代码。考虑到计算机语言的主要目的是为了让人们理解(编译器处理平衡设置对它们不利,除非你使用汇编语言、机器码或适当命名的 Brainf**k),这将表明该语言存在缺陷。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接