【问题标题】:Cannot assign to item because it is a foreach iteration variable [duplicate]无法分配给项目,因为它是一个 foreach 迭代变量 [重复]
【发布时间】:2020-01-15 21:25:09
【问题描述】:

为什么我们不能在 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# 代码确实可以编译" 不!这 not 等同于您在 foreach 循环中尝试执行的操作。在while 循环中,您创建了对对象的new 引用,然后为其分配了不同的值。 enumerator.Current = "d"; 也不起作用,出于同样的原因,enumerator.Current 是只读的。
  • @AhmedAbdelhameed 这与 Caius Jard 下面的回答背道而驰。
  • 在下面查看我的评论。另一方面,您的问题 is 类似于可能的重复问题,并且那里接受的答案回答了“为什么”问题。简单的答案是“因为迭代器是只读的”。欲了解更多信息,这里是another possible duplicate。专门检查那里的第二个答案,以了解有关它为什么是只读的更多信息。
  • @AhmedAbdelhameed 下面是一个示例,它显示了一个指向与 Current 相同的对象的局部变量:stackoverflow.com/questions/8898925/…
  • 在这种情况下,标记的重复项应该足以解决您的问题,以及 Stack Overflow 上的许多其他类似问题。事实是,如果这真的是你的意思,那么这个问题并不是很有用,因为它总是会有相同的答案:“因为这是编译器强制执行的语言规则”。你也可以问诸如“为什么internal 隐藏我的程序集之外的代码中的成员”,或者“为什么使用yield return 的方法必须具有IEnumerableIEnumerator、@987654333 的返回类型@ 或 IEnumerator&lt;T&gt;"?

标签: c# foreach iterator ienumerable ienumerator


【解决方案1】:

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

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 循环迭代器变量可以防止伤害/意外后果/晦涩的错误,没有我可以辨别的缺点。如果你想要一个变量,你可以重新分配;再做一个

【讨论】:

  • 我根据后续调查进行了编辑,我最初的记忆是正确的;使用了变量
  • 仅供参考,您编写的等效代码仅适用于 C# 4 及更低版本(Visual Studio 2010 及更早版本)。在 C# 5 和更高版本(visual studio 2012 和更高版本)中,string e; 在 while 循环内部而不是在其外部声明。请参阅stackoverflow.com/questions/271440/… 和问题中的 cmets
  • 好吧,我确实说我使用了哪个编译器...它相对于问题本身来说是相对附属的(这可能应该作为题外话关闭,因为它本质上是在征求评论关于编译器团队做出决定的原因)
  • @Backwards_Dave:你说“我应该能够更新那个变量”。不,你不应该。它是一个只读的局部变量,所以您应该能够更新它。
  • @Backwards_Dave:所以这里有一个有趣的问题供您考虑。假设循环包含await,并且循环变量在await 之前和之后都被访问。尝试反编译;你现在对循环变量的具体化有什么发现?
【解决方案2】:

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

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

我写了一封article,其中包含了我在了解IEnumerable/foreach 时提出的所有问题/答案。

【讨论】:

  • 反对者是否愿意发表评论?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-05
  • 2020-06-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多