可变中断范围

本文关键字:范围 中断 | 更新日期: 2024-10-18 14:16:56

为什么会出现's' does not exist in the current context错误(如预期):

public static void Main()
{
    foreach(var i in new[]{1, 2, 3}) {
        int s = i;
    }
    Console.WriteLine(s);
}

(视频)

但这会产生's' cannot be redeclared错误吗?

public static void Main()
{
    foreach(var i in new[]{1, 2, 3}) {
        int s = i;
    }
    int s = 4;
}

(视频)

第一个错误告诉我s不存在于foreach之外,这是有道理的,但第二个错误表明情况并非如此。为什么(以及如何!?)我需要从子作用域访问变量?

可变中断范围

第一个错误告诉我s不存在于foreach之外,这是有意义的

的确,这是对的。

但第二个错误表明情况并非如此。

不,不是。它告诉您,不能声明第一个(嵌套)变量s,因为第二个围中。

根据C#5规范,第3.7节:

•局部变量声明(§8.5.1)中声明的局部变量的范围是发生声明的块。

是的,它向上延伸到封闭的{。,大体上

然后从第8.5.1节:

在局部变量声明中声明的局部变量的作用域是发生声明的块。在局部变量的局部变量声明符之前的文本位置引用局部变量是错误的在局部变量的作用域内,声明另一个具有相同名称的局部变量或常量是编译时错误

最后一部分(重点是我的)是为什么你会出错。

为什么(以及如何!?)我需要从子作用域访问变量?

不确定你在这里的意思,但规则基本上是为了让你更难写难读或脆弱的代码。这意味着向上或向下移动变量声明(但仍在同一块中,处于同一嵌套级别)会产生有效但含义不同的代码的地方更少。

好的,首先考虑以下简单的类实现。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TestNamespace
{
    class ILOrderTest
    {
        public int DeclarationTests()
        {
            int intDeclaredAtTop = 0;
            for (int intDeclaredInForLoopDef = 0; intDeclaredInForLoopDef < 10; intDeclaredInForLoopDef++)
            {
                int intDeclaredInForLoopBody = intDeclaredInForLoopDef;
                intDeclaredAtTop = intDeclaredInForLoopBody;
            }
            int intDeclaredAfterForLoop;
            intDeclaredAfterForLoop = intDeclaredAtTop;
            return intDeclaredAfterForLoop;
        }
    }
}

正如我们所看到的,许多变量在我们方法的不同位置被声明,可以假设当C#解释器读取我们的文件时,它将以这样一种方式组织IL,即在我们编写变量定义代码的地方定义声明的对象。

然而,在编译和检查了我们的IL之后,我们看到了一个完全不同的故事。

类别ILOrderTest IL

.method public hidebysig 
    instance int32 DeclarationTests () cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 38 (0x26)
    .maxstack 2
    .locals init (
        [0] int32 intDeclaredAtTop,
        [1] int32 intDeclaredInForLoopDef,
        [2] int32 intDeclaredInForLoopBody,
        [3] int32 intDeclaredAfterForLoop,
        [4] int32 CS$1$0000,
        [5] bool CS$4$0001
    )
    IL_0000: nop
    IL_0001: ldc.i4.0
    IL_0002: stloc.0
    IL_0003: ldc.i4.0
    IL_0004: stloc.1
    IL_0005: br.s IL_0011
    // loop start (head: IL_0011)
        IL_0007: nop
        IL_0008: ldloc.1
        IL_0009: stloc.2
        IL_000a: ldloc.2
        IL_000b: stloc.0
        IL_000c: nop
        IL_000d: ldloc.1
        IL_000e: ldc.i4.1
        IL_000f: add
        IL_0010: stloc.1
        IL_0011: ldloc.1
        IL_0012: ldc.i4.s 10
        IL_0014: clt
        IL_0016: stloc.s CS$4$0001
        IL_0018: ldloc.s CS$4$0001
        IL_001a: brtrue.s IL_0007
    // end loop
    IL_001c: ldloc.0
    IL_001d: stloc.3
    IL_001e: ldloc.3
    IL_001f: stloc.s CS$1$0000
    IL_0021: br.s IL_0023
    IL_0023: ldloc.s CS$1$0000
    IL_0025: ret
} // end of method ILOrderTest::DeclarationTests

请注意,我们方法的所有对象都已收集在一起,并在方法顶部的.locals init...调用中初始化。如果我找到时间,我可以进一步挖掘为什么。Net IL是这样组织的,但如果我必须做出一个有根据的猜测,那就是变量声明涉及一定程度的开销,并且每次声明新对象时,将特定范围内的所有变量捆绑在一起可能会节省一些CPU周期,而不是对.locals init...进行多次调用。

我希望这能进一步澄清。从规范的定义来看,Jon的答案是正确的,但这可能会让我们了解为什么规范是这样写的。

C#在这方面与Java类似:它不允许在不同级别的作用域中声明具有相同名称的变量,即使该变量的第一个实例超出了作用域。

可以在C和C++中执行此操作。

我认为这是因为一个学派认为,如果允许这种行为,代码可能会变得难以理解/不稳定。

如果您希望有两个名称为s的不同变量,您可以执行以下操作:

static void Main(string[] args) {
    foreach(var i in new[]{1, 2, 3}) {
        int s = i;
    }
    // better describe what you do in this scope
    // (and let others know that this is not just an ordinary if-clause)
    {
        int s = 4;
    }
}

但在大多数情况下,您需要在作用域之外声明s,并使用默认值进行初始化:

static void Main(string[] args) {
    int s = default(int);
    foreach(var i in new[] { 1, 2, 3 }) {
        s = i;
        //use s in loop
    }
    //use s after loop
}