首页 > 解决方案 > 无法分配给项目,因为它是一个 foreach 迭代变量

问题描述

为什么我们不能在 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.

标签: c#foreachiteratorienumerableienumerator

解决方案


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

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# 都会以几种方式发展。新的处理结构当然可以添加到语言中,但“语法糖”通常是通过让编译器能够将您编写的代码翻译成已经存在的功能(例如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 循环迭代器变量可以防止伤害/意外后果/晦涩的错误,没有我可以辨别的缺点。如果你想要一个变量,你可以重新分配;再做一个


推荐阅读