为什么我们不能给foreach迭代变量赋值,但是我们可以通过访问器完全修改它呢?

81

我只是好奇:下面的代码将无法编译,因为我们无法修改foreach循环迭代变量:

        foreach (var item in MyObjectList)
        {
            item = Value;
        }

但是以下代码将会编译并运行:

        foreach (var item in MyObjectList)
        {
            item.Value = Value;
        }
为什么第一个无效,而第二个可以在底部执行相同的操作(我在寻找正确的英语表达方式,但我不记得它是什么。在底部...?^^)

5
我认为你的意思是“深入了解”,但实际上它完全不同。 - Jon Skeet
@GianT971,您能描述一下您在setter中如何“做同样的事情”吗?this = value - jv42
@JonSkeet 是的,在底层实现中^^。好的,所以item实际上是对象的引用,而不是对象本身,我现在明白了。 - GianT971
1
@jv42 你说得完全正确,我忘记了两个所有属性值都相同的对象并不意味着它们相等。 - GianT971
9个回答

52

foreach是一个只读迭代器,可以动态地迭代实现了IEnumerable接口的类,每个foreach循环将调用IEnumerable以获取下一个项,所得到的项是只读引用,您不能重新分配它,但是简单地调用item.Value即可访问它并将某些值分配给一个可读/写属性,但仍然是item的只读引用。


6
虽然该描述基本准确,但不完全正确。编译器只需要你要迭代的类型实现T GetEnumerator(), bool MoveNext()和一个T Current属性(假设你想创建自己的枚举器),而不是强制实现IEnumerable接口。因此,_foreach是一个只读迭代器,可以动态地迭代实现IEnumerable的类。 - Joseph Woodward
3
@JosephWoodward: “虽然正确,但不是严格意义上的真实情况。” Joseph,这是一种Yoda级别的陈述,绝对会进入我的历史前十名! :D - Jpsy
2
迭代变量为什么要设为只读,当你可以修改它所引用的对象呢?如果你想要修改变量的引用,可以通过去掉语法糖,使用 while 循环并调用 MoveNext() 等方法来实现。 - David Klempfner
因为您可以单独管理属性的访问级别,例如使某些属性仅具有getter而没有setter。它允许对对象内部的属性进行精细访问。 - Bon

38
第二个例子并不完全相同。它没有改变变量item的值,而是改变了该值所指向的对象的属性。只有当item是可变值类型时,这两个例子才会等效。然而可变值类型是有问题的,应该避免使用(它们的行为可能出乎开发者的意料)。与此类似的还有以下代码:
private readonly StringBuilder builder = new StringBuilder();

// Later...
builder = null; // Not allowed - you can't change the *variable*

// Allowed - changes the contents of the *object* to which the value
// of builder refers.
builder.Append("Foo");

更多信息请查看我的有关引用和值的文章(英文原文链接)


32

在枚举集合时无法修改它。第二个示例只更新对象的属性,这是完全不同的操作。

如果需要添加/删除/修改集合中的元素,请使用 for 循环:

for (int i = 0; i < MyObjectList.Count; i++)
{
    MyObjectList[i] = new MyObject();
}

1
但是如果代码编译通过,它也不会修改集合,因为迭代变量是局部变量,你只会更新该局部变量指向的引用。 - David Klempfner

12

如果你查看语言规范,就会明白为什么这样不起作用:

规范指出foreach被扩展为以下代码:

 E e = ((C)(x)).GetEnumerator();
   try {
      V v;
      while (e.MoveNext()) {
         v = (V)(T)e.Current;
                  embedded-statement
      }
   }
   finally {
      … // Dispose e
   }

正如您所看到的,当前元素用于调用MoveNext()。因此,如果更改当前元素,则代码将“丢失”,无法迭代集合。 因此,如果您看到编译器实际生成的代码,将元素更改为其他内容是没有意义的。


3
据我理解,变量v将持有当前元素的引用,而变量e则持有迭代器的引用。因此,编译器可以将任何我们想要的内容赋给v,而e仍将持有迭代器。这样,在将元素转换为v变量时,foreach循环中的每个元素变量都可以被分配新的引用,而不是e变量。 - Michel Feinstein

5

可以将item变为可变的。我们可以改变代码生成方式,使得:

foreach (var item in MyObjectList)
{
  item = Value;
}

变成了等价于:
using(var enumerator = MyObjectList.GetEnumerator())
{
  while(enumerator.MoveNext())
  {
    var item = enumerator.Current;
    item = Value;
  }
}

你只需要编译代码,但不会影响集合。

问题在于这段代码:

foreach (var item in MyObjectList)
{
  item = Value;
}

有两种合理的方式可以让人们思考这个问题。一种是item仅仅是一个占位符,更改它与在以下代码中更改item没有什么区别:

for(int item = 0; item < 100; item++)
    item *= 2; //perfectly valid

另一方面,更改项实际上会更改集合。
在前一种情况下,我们只需将项分配给另一个变量,然后对其进行操作,因此没有损失。在后一种情况下,这是被禁止的(或者至少,在迭代通过它时不能期望更改集合,尽管不是所有枚举器都必须实施),并且在许多情况下提供可能是不可能的(取决于可枚举的性质)。
即使我们认为前一种情况是“正确”的实现,它可以被人类合理地解释为两种不同的方式,这已经足够理由避免允许它,特别是考虑到我们可以轻松地在任何情况下解决这个问题。

虽然这似乎是“实际”的答案,但我认为后一种解释根本不合理。 - Zeus

1

因为第一个不太有意义。变量item由迭代器控制(在每次迭代中设置)。您不应该需要更改它-只需使用另一个变量:

foreach (var item in MyObjectList)
{
    var someOtherItem = Value;

    ....
}

关于第二个问题,有一些有效的用例- 您可能希望在汽车枚举上进行迭代,并在每个汽车上调用.Drive(),或设置car.gasTank = full;

1

关键是在迭代集合时不能修改集合本身。 修改迭代器返回的对象是绝对合法和常见的。


1

因为这两者不同。在进行第一个操作时,您可能希望更改集合中的值。但是,由于foreach的工作方式,无法实现此操作。它只能从集合中检索项目,而不能设置它们。

但是,一旦您拥有该项,它就像任何其他对象一样,可以以任何(允许的)方式进行修改。


1
请使用for循环代替foreach循环并赋值,这样会起作用。
foreach (var a in FixValue)
{
    for (int i = 0; i < str.Count(); i++)
    {
       if (a.Value.Contains(str[i]))
           str[i] = a.Key;
    }
 }

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