How is the iteration variable readonly?
Asked Answered
N

2

3

In 8.8.4 of the C# specification, it provides this example:

A foreach statement of the form

foreach (V v in x) embedded-statement

is then expanded to:

{
    E e = ((C)(x)).GetEnumerator();
    try {
        V v;
        while (e.MoveNext()) {
            v = (V)(T)e.Current;
            embedded-statement
        }
    }
    finally {
        … // Dispose e
    }
}

It also says:

The iteration variable corresponds to a read-only local variable with a scope that extends over the embedded statement.

The variable v is read-only in the embedded statement.

How is the iteration variable made readonly?

In C# you can't use readonly here, and const doesn't work either.

Here is an example I made.

I viewed the CIL code but can't see anywhere where it makes the iteration variable readonly:

C#:

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

        foreach (string item in enumerable)
        {
            string x = item;
        }
    }
}

CIL:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 80 (0x50)
    .maxstack 3
    .entrypoint
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.List`1<string> enumerable,
        [1] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>,
        [2] string item,
        [3] string x
    )

    IL_0000: nop
    IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<string>::.ctor()
    IL_0006: dup
    IL_0007: ldstr "a"
    IL_000c: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    IL_0011: nop
    IL_0012: dup
    IL_0013: ldstr "b"
    IL_0018: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    IL_001d: nop
    IL_001e: stloc.0
    IL_001f: nop
    IL_0020: ldloc.0
    IL_0021: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<string>::GetEnumerator()
    IL_0026: stloc.1
    .try
    {
        IL_0027: br.s IL_0035
        // loop start (head: IL_0035)
            IL_0029: ldloca.s 1
            IL_002b: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::get_Current()
            IL_0030: stloc.2
            IL_0031: nop
            IL_0032: ldloc.2
            IL_0033: stloc.3
            IL_0034: nop

            IL_0035: ldloca.s 1
            IL_0037: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::MoveNext()
            IL_003c: brtrue.s IL_0029
        // end loop

        IL_003e: leave.s IL_004f
    } // end .try
    finally
    {
        IL_0040: ldloca.s 1
        IL_0042: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>
        IL_0048: callvirt instance void [mscorlib]System.IDisposable::Dispose()
        IL_004d: nop
        IL_004e: endfinally
    } // end handler

    IL_004f: ret
} // end of method Program::Main
Nugatory answered 15/9, 2019 at 3:38 Comment(1)
It looks like it's just the compiler enforcing that rule. If you manually lower your foreach to use Enumerator, you can reassign all you want.Possum
L
1

There is special-case code in the compiler which enforces the read-only constraint on the iteration variable in a foreach block. It does not correspond to any modifier which is exposed in the language, so you can't explicitly declare local variables as read-only outside of this particular syntax.

Conceptually, this constraint is applied before the expansion. That is, if there are any assignments to the iteration variable, the compiler generates an error. Otherwise the code is expanded. In the expanded code there is no particular constraints on v since it is just a regular local variable. Therefore the constraint does not exist in the IL either.

So why is there this special-case read-only constraint with the foreach-syntax? Only the language designers can answer that, but I would guess it is just to avoid confusion. If the iterator variable was assignable, you might think you were able to modify the actual collection that way, but nothing would actually happen, since the underlying enumerator is read-only.

Louanneloucks answered 19/9, 2019 at 8:52 Comment(0)
B
5

The iteration variable is read-only because it is an error to write to it. Give it a try, and you'll see.

It doesn't make a readonly field, and the documentation does not say that it makes a readonly field. It cannot possibly be a readonly field because it is not a field.

Now, here is a subtle question. Suppose v is of mutable value type, and you call a method on the type which mutates a field of this, passing v. Make a prediction about what happens. Now try it; were you right? Can you explain what happened? What do you think now about the claim that v is "read-only"? Would you say that this is a bug, or the right behavior?

Now try the same thing with a readonly field, and see what the results are. Do you think this is the right behaviour?

Bogus answered 15/9, 2019 at 3:52 Comment(13)
I understand that it should be readonly because if it's a value type, you'd end up mutating a copy, not the actual instance referred to by Current. But I still don't know how the readonly is enforced. Is it just some rule in the compiler that stops you from modifying the variable? If so, what exactly is this "rule", how is it imlemented?Nugatory
For a second I thought I was reading challenge section after completing a chapter of a tutorial.Ferity
@Eric Lippert the example provided by the C# spec doesn't seem very accurate. v = (V)(T)e.Current; looks like a local variable that I'd be able to modify. How would you represent the "readonly" rule in C#? Is it even possible?Nugatory
@FCin: That was the idea, yes.Bogus
@Backwards_Dave: It sounds like your question is "the C# specification describes the meaning of the foreach loop by creating some equivalent code, but further stating in the text describing the code that the variable is read-only; is there a way to create some equivalent code where the variable is read-only?" No. If there were, then we would not have had to add the extra text explaining that it is read-only.Bogus
The point of my final paragraphs was to get you to think about your conjecture "is it just some rule in the compiler that stops you from modifying the variable?" Yes. And moreover, that rule does not change the classification of the variable as a variable, which means that it is not copied by value when passed by reference, unlike a real readonly field.Bogus
If you want to know how the rule is implemented, read the compiler source code; it's open sourced so that you can read it.Bogus
@EricLippert could you elaborate on what you mean by "Now try the same thing with a readonly field"?Nugatory
@EricLippert I'm struggling to understand what you mean by "that rule does not change the classification of the variable as a variable, which means that it is not copied by value when passed by reference, unlike a real readonly field". Do you mean to say that readonly fields are always passed by value even if it's a ref type? If so, how is that relevant to this question?Nugatory
@Backwards_Dave: You have discovered that the read-only variable in the foreach loop is only read-only because it may not be written to directly; a variable of struct type may still be written to when used indirectly via ref. But this is not true of readonly fields. A readonly field is not classified as a variable at all, and therefore must be copied into a variable when passed by ref. It is relevant to the question because you asked about the semantics of readonly fields as they compare to the semantics of a loop variable. I'm answering your question.Bogus
@Backwards_Dave: I did not say that a readonly field is passed by value even when it is of reference type. I said that it is treated as a value when passed by reference, that is, using ref; this difference becomes observable in the case of a mutable value type. Obviously there are no mutable values of reference type because references are not mutable; there are only mutable instances of reference type.Bogus
@EricLippert "I did not say that a readonly field is passed by value even when it is of reference type. I said that it is treated as a value when passed by reference, that is, using ref" - The compiler prevents you from passing readonly fields using ref.Nugatory
@Backwards_Dave: Good catch; I misspoke and should not have said "using ref". What I'm alluding to is: the compiler implicitly passes by ref any this of a value type because the value type might be mutable. That includes readonly fields. This then becomes visible, because a readonly field of mutable value type is copied to a fresh variable every time you implicitly pass it by ref. This can be quite confusing.Bogus
L
1

There is special-case code in the compiler which enforces the read-only constraint on the iteration variable in a foreach block. It does not correspond to any modifier which is exposed in the language, so you can't explicitly declare local variables as read-only outside of this particular syntax.

Conceptually, this constraint is applied before the expansion. That is, if there are any assignments to the iteration variable, the compiler generates an error. Otherwise the code is expanded. In the expanded code there is no particular constraints on v since it is just a regular local variable. Therefore the constraint does not exist in the IL either.

So why is there this special-case read-only constraint with the foreach-syntax? Only the language designers can answer that, but I would guess it is just to avoid confusion. If the iterator variable was assignable, you might think you were able to modify the actual collection that way, but nothing would actually happen, since the underlying enumerator is read-only.

Louanneloucks answered 19/9, 2019 at 8:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.