Cannot assign to item because it is a foreach iter

2020-05-07 07:15发布

Why can't we assign a value to the local variable in a foreach loop?

I understand this would not update the collection because we are just changing what object the local variable refers to.

If you implement the same functionality using a while loop, it allows you to update the local variable.

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();
        }
    }
}

EDIT: This question is different to the possible duplicate because it's asking why we can't modify the iteration variable when behind the scenes it's just a local variable pointing to the same object as enumerator.Current.

2条回答
叼着烟拽天下
2楼-- · 2020-05-07 07:50

According to Eric Lippert's answer, The iteration variable is read-only because it is an error to write to it.

Looks like there is some rule in the compiler that stops you from compiling code that attempts to modify the iteration variable, even though it's not marked as readonly behind the scenes (it can't anyway because it's a local var).

查看更多
男人必须洒脱
3楼-- · 2020-05-07 07:52

I modified your code so they're actually equivalent of each other:

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";
        }
    }
}

As you can see, in neither form do we assign to the readonly enumerator.Current


When you write a foreach, the compiler rewrites your code for you, so that it looks like this:

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();
}

With each passing version, C# evolves in a couple of ways. New processing structures can certainly be added to the language, but often "syntactic sugar" is an evolution achieved by having the compiler able to translate the code you write into a feature that already exists, such as foreach(x in y) becoming y.GetEnumerator(); while(..){ x = enumerator.Current; ... }.

Similarly, string interpolation $"There are {list.Count:N2} items" being transformable to the implemented-firststring.Format("There are {0:N2} items, list.Count")`.

Indeed, if one considers the job of the compiler to transform something that is written into something else that is already written, then even syntactic sugaring is an example of a new processing structure, because everything is already written (right the way down to the hardware)


To examine how the compiler rewrites a foreach, I wrote two methods:

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();
    }

}

I compiled with csc.exe that came with .NET SDK 4.7.2, then used ILDASM to visualize the generated MSIL (screenshot to show the side by side diff). The initial part of the method, setup, variables declare etc are identical but for a no-op:

enter image description here

As are the loop bodies:

enter image description here

The only difference discernible is in the disposing; I wasn't too interested in chasing down the reasons why.

So, it's clear that the compiler rewrites the method in the way shown. Your original question can only really be answered by the person that wrote the compiler, but it seems logical to me that it's an extension of the prohibition against assigning to enumerator.Current - it wouldn't make sense to prevent that but allow you to assign to the variable equivalent of it declared in your foreach loop. It's also clear that it's a special case assessed directly, from the way the error message talks abut the foreach loop specifically.

I'd also say (opinion part) that preventing assignment to the foreach loop iterator variable prevents harm/unintended consequences/obscure bugs, with no drawback that I can discern. If you want a variable you can reassign; make another one

查看更多
登录 后发表回答