【问题标题】:How is the iteration variable readonly?迭代变量如何只读?
【发布时间】:2020-01-16 08:28:29
【问题描述】:

在 C# 规范的 8.8.4 中,它提供了这个例子:

形式的foreach语句

foreach (V v in x) embedded-statement

然后扩展为:

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

它还说:

迭代变量对应一个只读局部变量 扩展嵌入语句的范围。

变量v在嵌入语句中是只读的。

如何将迭代变量设为只读?

在 C# 中你不能在这里使用 readonly,而且 const 也不起作用。

这是我做的一个例子。

我查看了 CIL 代码,但看不到它使迭代变量只读的任何地方:

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

【问题讨论】:

  • 看起来它只是执行该规则的编译器。如果您手动降低 foreach 以使用 Enumerator,您可以重新分配所有您想要的。

标签: c# foreach iterator ienumerable cil


【解决方案1】:

编译器中有特殊情况代码,它对foreach 块中的迭代变量强制执行只读约束。它不对应于语言中公开的任何修饰符,因此您不能在此特定语法之外将局部变量显式声明为只读。

从概念上讲,此约束应用于在扩展之前。也就是说,如果对迭代变量有任何赋值,编译器就会产生错误。否则代码被扩展。在扩展代码中,v 没有特别的限制,因为它只是一个常规的局部变量。因此,IL 中也不存在约束。

那么为什么foreach-syntax 中存在这种特殊情况的只读约束?只有语言设计者才能回答这个问题,但我想这只是为了避免混淆。如果迭代器变量是可赋值的,您可能认为您可以通过这种方式修改实际的集合,但实际上不会发生任何事情,因为底层枚举器是只读的。

【讨论】:

    【解决方案2】:

    迭代变量是只读的,因为写入它是错误的。试试看,你会看到的。

    它没有创建readonly 字段,文档也没有说它创建了readonly 字段。它不可能是 readonly 字段,因为它不是字段。

    现在,这是一个微妙的问题。假设v 是可变值类型,并且您在该类型上调用一个方法来改变this 的字段,并传递v。预测会发生什么。现在试试看;你是对的吗?你能解释发生了什么吗?您现在如何看待v 是“只读”的说法?你会说这是一个错误,还是正确的行为?

    现在用readonly 字段尝试相同的操作,看看结果如何。你认为这是正确的行为吗?

    【讨论】:

    • 在完成教程的一章后,我以为我正在阅读挑战部分。
    • @Backwards_Dave:听起来您的问题是“C# 规范通过创建一些等效代码来描述 foreach 循环的含义,但在描述变量的代码中进一步说明该变量是只读的; 有没有办法在变量是只读的情况下创建一些等效的代码?”不。如果有,那么我们就不必添加额外的文本来解释它是只读的。
    • 我最后几段的重点是让您思考您的猜想“只是编译器中的某些规则阻止您修改变量吗?”是的。而且,该规则不会改变变量作为变量的分类,这意味着它在通过引用传递时不会按值复制,不像真正的readonly字段
    • @Backwards_Dave:你发现foreach循环中的只读变量是只读的,因为它可能不会被直接写入;当通过ref 间接使用时,结构类型的变量仍可能被写入。但readonly 字段并非如此。 readonly 字段根本不归类为变量,因此在通过ref 传递时必须复制到变量中。这与问题相关因为您询问了readonly 字段的语义,因为它们与循环变量的语义进行了比较。我正在回答你的问题。
    • @Backwards_Dave:我没有说readonly 字段即使是引用类型也是按值传递的。我说通过引用传递的时候被当作一个值,也就是使用ref;在可变值类型的情况下,这种差异变得显而易见。显然,引用类型不存在可变的,因为引用是不可变的;只有可变的实例的引用类型。
    猜你喜欢
    • 2014-11-21
    • 2010-12-02
    • 2016-01-11
    • 1970-01-01
    • 1970-01-01
    • 2015-01-23
    • 2015-08-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多