变量作用域问题

4

为什么这会出现一个'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不能被重新声明”的错误? (ideone)
public static void Main()
{
    foreach(var i in new[]{1, 2, 3}) {
        int s = i;
    }

    int s = 4;
}

(ideone)

第一个错误告诉我在 foreach 之外不存在变量 s,这是有道理的,但第二个错误则不然。为什么(以及如何!?)我需要从子范围访问变量呢?


因为你在第二个中尝试重新声明s,这不正常吗? - Soner Gönül
3
编译器试图保护你免受疏忽错误的影响。因此,即使该变量不可访问,你也无法在同一方法中声明已经被声明过的变量。 - Tim Schmelter
请参见"问题标题中是否应包含“标签”?",共识是“不应该”! - user57508
方法体中没有词法作用域。 - leppie
@Jaxo:你应该问自己:为什么我需要在foreach后面使用与foreach中相同的变量名?你从中获得了什么?在我看来,除了混淆它们之外,没有任何好处,这会成为一个错误的重要来源。如果以后想将其移动到方法的顶部,它也无法编译。我喜欢编译器有时指导我编写更好的代码(就健壮性、可维护性和可读性而言)。 - Tim Schmelter
显示剩余2条评论
4个回答

12
第一个错误告诉我,s在foreach之外不存在,这很合理。
确实 - 它是正确的。
但第二个错误信息则否定了这一点。
不,它没有。它告诉你无法声明第一个(嵌套的)变量“s”,因为第二个已经在作用域中。您无法在声明之前访问它,但它在整个块的范围内。
来自C# 5规范,第3.7节:
•在本地变量声明(§8.5.1)中声明的局部变量的范围是发生声明的块。
所以,是的,它向上延伸到封闭的{基本上。
然后从第8.5.1节中提取:
在本地变量声明中声明的局部变量的范围是发生声明的块。在文本位置引用局部变量之前的局部变量声明符号是错误的。在局部变量的作用域内,声明具有相同名称的另一个局部变量或常量是编译时错误。
最后一部分(重点是我的)就是您收到错误的原因。
为什么(如何!?)我需要从子范围访问变量?
不确定您的意思,但该规则基本上使得编写难以阅读或脆弱的代码更加困难。这意味着移动变量声明的位置会减少产生有效但意义不同的代码的位置(但仍在同一块中,嵌套级别相同)。

@Jaxo:你期望它会有什么不同?忽略IL - 那是一个实现细节。只关注语言规则即可。 - Jon Skeet
嗨Jon,我同意“注意语言规则”这个规则,但如果有人好奇为什么,那么他们可能会问,这个规则首先存在的原因是什么。我将发布一个次要答案,概述幕后发生的事情。由于我不是MS的工程师,所以我只能猜测为什么会发生这种情况。请继续关注。 - RLH
1
@RLH:这个“为什么”与IL完全无关。语言本来可以被设计成允许这样做并生成适当的IL。之所以将语言设计成这样,是为了让你更难自己给自己惹麻烦——而不是因为IL的限制。 - Jon Skeet
当然可以 - 但这种情况最多只会在警告中出现,而不是因为编译器不喜欢我的变量名而完全停止编译。 - Scott
2
@Jaxo:我会看一下注释规范中是否有任何内容,但基本上是因为否则代码可能会非常混乱。 (我相信我曾经在某个地方看到过这样的写法,但我现在无法记起来源。它很可能是注释规范或Eric Lippert的博客文章之一。)就我个人而言,我完全满意它成为编译时错误。这只涉及局部变量,因此不像其他代码可以在您不做任何事情的情况下破坏您的代码(例如,基类引入另一个成员,导致警告说您应该使用“new”)。 - Jon Skeet
显示剩余3条评论

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的答案按规范定义是正确的,但这可能会阐明为什么规范是以这种方式编写的。

哇!你可能是对的,编译器试图过于激进地进行优化,这会在尝试将具有相同签名的两个变量放在同一位置时出现问题。非常好的解释,谢谢! - Scott

0
如果你想用两个不同的变量名来表示变量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
}

0

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

在C和C++中,您可以这样做。

我认为这是一种思想流派的结果,该流派认为如果允许这种行为,则代码可能会变得难以理解/不稳定。


那不是叫做阴影吗? - Tarik
我完全同意编译器试图通过代码可读性来帮助我们。我经常写Javascript,这种事情在这里可以正常工作:http://jsfiddle.net/zv8bjagf/1/。 - Scott

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接