0

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.

David Klempfner
  • 8,700
  • 20
  • 73
  • 153
  • 1
    _"The equivalent C# code does compile"_ Nope! That is _**not**_ equivalent to what you're trying to do in the `foreach` loop. In the `while` loop, you created a _new_ reference to the object and then assigned a different value to it. `enumerator.Current = "d";` wouldn't work either, and for the same reason, that is, `enumerator.Current` is read-only. – 41686d6564 stands w. Palestine Sep 14 '19 at 06:53
  • @AhmedAbdelhameed this goes against what the answer below by Caius Jard says. – David Klempfner Sep 14 '19 at 07:14
  • 1
    See my comment below. On another note, your question _is_ similar to the possible duplicate and the accepted answer there answers the "why" question. The simple answer is "because the iterator is read-only". For more information, here's [another possible duplicate](https://stackoverflow.com/q/4004755/4934172). Check the second answer there specially for more about why it's read-only. – 41686d6564 stands w. Palestine Sep 14 '19 at 07:30
  • @AhmedAbdelhameed Here is an example where it shows a local variable pointing to the same object as Current: https://stackoverflow.com/questions/8898925/is-there-a-reason-for-cs-reuse-of-the-variable-in-a-foreach/ – David Klempfner Sep 14 '19 at 07:55
  • The real duplicate is https://stackoverflow.com/questions/4004755/why-is-foreach-loop-read-only-in-c-sharp. But, see also https://stackoverflow.com/questions/7838079/why-cant-we-assign-a-foreach-iteration-variable-whereas-we-can-completely-modi and **especially** https://stackoverflow.com/questions/8898925/is-there-a-reason-for-cs-reuse-of-the-variable-in-a-foreach/ for additional related discussion on the topic. Making the variable itself read-only is an important element in improving code quality/reliability, but it's not a panacea. It's important to stay aware of other related pitfalls. – Peter Duniho Sep 15 '19 at 23:28
  • Frankly, the question is really "primarily opinion-based", except if someone from the C# language design team actually answers it (and even then, they are still just sharing the opinion that led to the design decision). That said, my take on it: in other loop constructs, modifying the loop variable can have a direct effect on the behavior of the loop itself. But if you could modify a `foreach` loop variable, it wouldn't change the loop iteration behavior. C# design tends toward preventing you from getting into inexplicably counter-intuitive situations; making the variable read-only does that. – Peter Duniho Sep 15 '19 at 23:35
  • @PeterDuniho my question was "Why can't we assign a value to the local variable in a foreach loop?" and the answer is definitely not opinion based. The answer is that there is a rule in the compiler that prevents you from modifying the iteration variable, that's not opinion based, that's a fact. – David Klempfner Sep 16 '19 at 00:57
  • 1
    In that case, the marked duplicates should adequately address your question, along with the many other similar questions on Stack Overflow. Fact is, if that's really what you mean, the question isn't very useful, because it always will have the same answer: _"because that's the rules of the language, which the compiler enforces"_. You may as well ask things like "why does `internal` hide members from code outside my assembly", or "why does a method that uses `yield return` have to have a return type of `IEnumerable`, `IEnumerator`, `IEnumerable`, or `IEnumerator`"? – Peter Duniho Sep 16 '19 at 01:01
  • Either you're asking why the language is the way it is (opinion-based), or you're asking why the compiler is following the rules of the language (which has the obvious, non-useful answer of "that's what it's supposed to do") – Peter Duniho Sep 16 '19 at 01:03
  • @PeterDuniho my question came about because if you look at the code without syntactic sugar, it looks as if you should be able to modify the iteration variable. Usually to prevent a variable from being modified you need to use const or readonly. I was curious as to how this was achieved with a local var which can't be marked readonly. Now I know it's because of a rule in the compiler. – David Klempfner Sep 16 '19 at 01:10
  • @PeterDuniho: The problem with "why is the language this way?" ("or worse, why is the language NOT this way?") is not that it requires an *opinion*. The question could be phrased as "what were the pros and cons considered by the language design team, and what factors did they use in coming to the final decision?" which is a question about historical facts, not opinions. Now, those questions are problematic particularly in that a very small number of people on this site have the historical background to answer them accurately. – Eric Lippert Sep 16 '19 at 16:59
  • @PeterDuniho: The real problem with "why" question is that they are *vague* and therefore hard to answer. The original poster here seems to be actually asking "where is this rule implemented?" and not "why was this rule considered to be a good idea?" Other people who ask "why" questions are really asking "what line of the specification defines this behaviour?" and some are asking "is this feature consistent with the design of this other feature?" and so on. "Ask a more specific question" is my usual pushback on this sort of thing. – Eric Lippert Sep 16 '19 at 17:01
  • @Eric: _"The question could be phrased as "what were the pros and cons considered by the language design team, and what factors did they use in coming to the final decision?" which is a question about historical facts, not opinions"_ -- maybe it's splitting hairs, but...while all those pros/cons are relevant, ultimately a person used those facts to arrive at an opinion. A different person with the same facts might come to a different conclusion. That said, yes..."unclear" or "too broad" would be equally applicable reasons to close, due to the various ways to interpret it. – Peter Duniho Sep 16 '19 at 17:47

2 Answers2

0

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

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • So the generated code uses a variable that points to the same object as enumerator.Current. Which means I should be able to update that variable and not change the underlying collection. – David Klempfner Sep 14 '19 at 07:10
  • `string element = (string)enumerator.Current;` Why? Is there a reference for that somewhere? Why wouldn't just `enumerator.Current` be sufficient? – 41686d6564 stands w. Palestine Sep 14 '19 at 07:24
  • @AhmedAbdelhameed I agree with you that enumerator.Current by itself should be sufficient, however every example I've seen so far has used a local var to store enumerator.Current. Where can I find a reliable source showing the equivalent lower level C# code for a foreach loop? – David Klempfner Sep 14 '19 at 07:30
  • Sorry guys, you're right and it's more clear to me what the actual question is. To best answer this it will require a demo of what these things both compile to - I haven't time to add that to the answer right now, but I will update it later – Caius Jard Sep 14 '19 at 07:39
  • @CaiusJard https://resnikb.wordpress.com/2009/06/17/c-lambda-and-foreach-variable/ shows code that stores enumerator.Current in a variable. – David Klempfner Sep 14 '19 at 08:21
  • The first answer in this question also has a local variable: https://stackoverflow.com/questions/57933398/converting-il-to-c-sharp-with-no-syntactic-sugar/ – David Klempfner Sep 14 '19 at 08:56
  • The answer by SaurabhJaiswal also has a local variable: https://stackoverflow.com/questions/5816776/is-foreach-purely-syntactic-sugar – David Klempfner Sep 15 '19 at 02:31
  • 1
    I've made an edit based on a follow-up investigation that my initial recollection was correct; a variable is used – Caius Jard Sep 15 '19 at 20:49
  • 1
    Just a FYI, the equivalent code you wrote is only true for C# 4 and lower (visual studio 2010 and older). In C# 5 and newer (visual studio 2012 and newer) the `string e;` gets declared inside of the while loop instead of outside of it. See https://stackoverflow.com/questions/271440/captured-variable-in-a-loop-in-c-sharp and the comments in the question – Scott Chamberlain Sep 15 '19 at 21:01
  • 2
    Well, I *did* say which compiler I used... It's relatively accessory to the question itself (which should probably be closed as off topic, because it's essentially asking for commentary on why the compiler team made the decisions they did) – Caius Jard Sep 15 '19 at 21:40
  • 2
    @Backwards_Dave: You say "I should be able to update that variable". No, you should not. It's a read-only local variable, so you should *not* be able to update it. – Eric Lippert Sep 16 '19 at 17:02
  • 2
    @Backwards_Dave: So here's an interesting question for you to consider. Suppose the loop contains an `await`, and the loop variable is accessed both before and after the `await`. Try decompiling that; what do you discover about the reification of the loop variable now? – Eric Lippert Sep 16 '19 at 17:04
0

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).

I wrote an article with all the questions/answers I came up with while learning about IEnumerable/foreach.

David Klempfner
  • 8,700
  • 20
  • 73
  • 153