无法分配给项目,因为它是 foreach 迭代变量

Cannot assign to item because it is a foreach iteration variable

为什么我们不能在foreach循环中给局部变量赋值?

我知道这不会更新集合,因为我们只是更改局部变量引用的对象。

如果您使用 while 循环实现相同的功能,它允许您更新局部变量。

class Program
{
    static void Main(string[] args)
    {
        IEnumerable<string> enumerable = new List<string> { "a", "b", "c" };

        //This foreach loop does not compile:
        foreach (string item in enumerable)
        {
            item = "d"; //"cannot assign to item because it is a foreach iteration variable"
        }

        //The equivalent C# code does compile and allows assinging a value to item:            
        var enumerator = enumerable.GetEnumerator();
        try
        {
            while (enumerator.MoveNext())
            {
                string item = (string)enumerator.Current;
                item = "d";
            }
        }
        finally
        {
            enumerator.Dispose();
        }
    }
}

编辑: 这个问题与可能的重复问题不同,因为它问的是为什么我们不能在幕后修改迭代变量它只是一个指向与 enumerator.Current.

相同对象的局部变量

我修改了你的代码,所以它们实际上是等价的:

class Program
{
    static void Main(string[] args)
    {
        IEnumerable<string> enumerable = new List<string> { "a", "b", "c" };
        foreach (string element in enumerable)
        {
            object item = element;
            item = "d"; 
        }

        IEnumerator enumerator = enumerable.GetEnumerator();
        while (enumerator.MoveNext())
        {
            object item = enumerator.Current;
            item = "d";
        }
    }
}

如您所见,我们在这两种形式中都没有分配给只读 enumerator.Current


当您编写 foreach 时,编译器会为您重写代码,使其看起来像这样:

IEnumerable<string> enumerable = new List<string> { "a", "b", "c" };

//you write
foreach (string element in enumerable)
{
    Console.WriteLine(element);
}

//the compiler writes
string element;   
var enumerator = enumerable.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        element = enumerator.Current;

        //begin your foreach body code
        Console.Write(element); 
        //end your foreach body code
    }
}
finally
{
    enumerator.Dispose();
}

随着每个版本的更新,C# 都会以多种方式发展。新的处理结构当然可以添加到语言中,但通常 "syntactic sugar" 是一种进化,通过让编译器能够将您编写的代码翻译成已经存在的功能,例如 foreach(x in y) 变成 y.GetEnumerator(); while(..){ x = enumerator.Current; ... }

类似地,字符串插值$"There are {list.Count:N2} items" being transformable to the implemented-firststring.Format("There are {0:N2} items, list.Count")`。

确实,如果考虑编译器的工作是将已写入的内容转换为已写入的其他内容,那么即使语法加糖也是一种新处理结构的示例,因为所有内容都已写入(对一直到硬件)


为了检查编译器如何重写 foreach,我写了两个方法:

static void ForEachWay(){


    var enumerable = new List<string>();
    enumerable.Add("a");
    enumerable.Add("b");
    enumerable.Add("c");

    //you write
    foreach (string element in enumerable)
    {
        Console.WriteLine(element);
    }

}

static void EnumWayWithLoopVar(){
    var enumerable = new List<string>();
    enumerable.Add("a");
    enumerable.Add("b");
    enumerable.Add("c");

    string e;
    var enumerator = enumerable.GetEnumerator();
    try{ 
        while (enumerator.MoveNext())
        {
            e = enumerator.Current;
            Console.Write(e); 
        }
    }finally{
        enumerator.Dispose();
    }

}

我使用 .NET SDK 4.7.2 附带的 csc.exe 进行编译,然后使用 ILDASM 可视化生成的 MSIL(显示并排差异的屏幕截图)。方法的初始部分、设置、变量声明等是相同的,但对于空操作:

循环体也是如此:

唯一可辨别的区别在于配置;我对追查原因不太感兴趣。

很明显,编译器按照所示方式重写了方法。您最初的问题只能由编写编译器的人真正回答,但对我来说这似乎是合乎逻辑的,因为它是禁止分配给 enumerator.Current 的扩展 - 阻止它是没有意义的,但允许你分配给在 foreach 循环中声明的等效变量。从错误消息具体讨论 foreach 循环的方式来看,很明显这是一种直接评估的特殊情况。

我还要说(意见部分)防止对 foreach 循环迭代器变量赋值可以防止 harm/unintended consequences/obscure 错误,而且我看不出有什么缺点。如果你想要一个变量,你可以重新分配;再做一个

根据 Eric Lippert 的 The iteration variable is read-only because it is an error to write to it

看起来编译器中有一些规则阻止你编译试图修改迭代变量的代码,即使它在幕后没有被标记为 readonly(无论如何它不能,因为它是本地变量)。

我写了一篇 article,其中包含我在学习 IEnumerable/foreach 时想到的所有 questions/answers。