为什么不能为foreach迭代变量赋值,而可以用访问器完全修改它?

本文关键字:访问 修改 foreach 不能 迭代 变量 赋值 为什么 | 更新日期: 2023-09-27 18:12:06

我只是对此感到好奇:以下代码将无法编译,因为我们无法修改foreach迭代变量:

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

但是下面的代码可以编译并运行:

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

为什么第一个无效,而第二个可以在下面做同样的事情(我正在寻找正确的英语表达,但我不记得了。在…?^ ^)

为什么不能为foreach迭代变量赋值,而可以用访问器完全修改它?

foreach是一个只读迭代器,它动态迭代实现IEnumerable的类,foreach中的每个循环将调用IEnumerable来获取下一个项目,您拥有的项目是只读引用,您不能重新分配它,但简单地调用item.Value是访问它并为读/写属性分配一些值,但项目的引用仍然是只读引用。

第二个程序完全不做同样的事情。它没有改变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");

更多信息请参阅我的文章

不能在枚举集合时修改它。第二个例子只更新对象的一个属性,这是完全不同的。

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

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

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

规范说明foreach扩展为以下代码:

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

可以看到,当前元素是用来调用moveext()的。因此,如果您更改当前元素,代码将"丢失"并且无法遍历集合。因此,如果你看到编译器实际生成的代码,将元素更改为其他内容是没有任何意义的。

有可能使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

另一个是更改item实际上会更改集合。

在前一种情况下,我们可以将item赋值给另一个变量,然后使用它,这样就不会有损失。在后一种情况下,这是被禁止的(或者至少,你不能期望在迭代一个集合时改变它,尽管它不要求被所有枚举器强制执行),并且在许多情况下不可能提供(取决于枚举对象的性质)。

即使我们认为前一种情况是"正确的"实现,事实是它可以被人类以两种不同的方式合理地解释,这是一个足够好的理由来避免允许它,特别是考虑到我们可以很容易地在任何情况下绕过它。

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

foreach (var item in MyObjectList)
{
    var someOtherItem = Value;
    ....
}
对于第二个,这里有一些有效的用例—您可能希望遍历汽车枚举并对每个汽车调用.Drive(),或者设置car.gasTank = full;

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

因为两个不相同。在执行第一个操作时,您可能希望更改集合中的值。但foreach的工作方式是不可能做到的。它只能从集合中检索项,不能设置项。

但是一旦你有了项目,它就和其他任何对象一样,你可以用任何(允许的)方式修改它

使用for循环而不是foreach循环和assign value。它会工作的

foreach (var a in FixValue)
{
    for (int i = 0; i < str.Count(); i++)
    {
       if (a.Value.Contains(str[i]))
           str[i] = a.Key;
    }
 }