C#变量作用域:'x'无法在此范围内声明,因为它会给'x'赋予不同的含义。

67
if(true)
{
    string var = "VAR";
}

string var = "New VAR!";

这将导致:

错误1:命名为“var”的局部变量不能在此范围内声明,因为它会赋予“var”不同的含义,而“var”已经在“子”范围中用于表示其他内容。

并没有什么特别的问题,但这难道不是明显错误吗?我和另一位开发人员想知道第一个声明是否应该在不同的作用域中,这样第二个声明就不能干扰第一个声明。

为什么C#无法区分两个作用域?第一个IF作用域不应该完全与方法的其余部分分开吗?

我无法从if之外调用var,所以错误消息是错误的,因为第一个var在第二个作用域中没有 relevancy。


3
实际编译器错误为 -无法在此作用域中声明名为“x”的本地变量,因为这将赋予“x”不同的含义,而该“父级或当前”作用域已经使用“x”表示其他内容。 - Kris Krause
1
类似于https://dev59.com/xHVC5IYBdhLWcg3wbghT的问题 - David Espart
5
我猜你正在使用C# 2.0,因为var是C# 3.0中的关键字。 - John Saunders
9
C# 3.0中,“var”不是保留关键字。当(1)它出现在本地变量声明(或使用块声明、foreach、for等)的类型中,且(2)当前范围内不存在名为“var”的类型时,“var”只表示“隐式类型本地变量”。自C# 1以来,C#从未添加过保留关键字;所有新关键字都是上下文关键字。除特定上下文外,它们只是合法标识符,只有在特定上下文中才具有关键字意义。 - Eric Lippert
当你想要插入一些代码并且希望最小化副作用时,这是非常有用的,因为你可以确信它不会与其他代码冲突。当截止日期即将到来时,我经常使用它。 - Ghasan غسان
显示剩余4条评论
3个回答

46

这里的问题主要是好的编程习惯以及防止无意中的错误。可以理解的是,C#编译器理论上可以设计得不存在这里不同范围之间的冲突。但我认为这样做会付出很大努力,收益却很小。

请考虑,如果父范围内var的声明在if语句之前,那么将会有一个无法解决的命名冲突。编译器根据作用域进行分析,而不是根据声明/使用的顺序,正如你所期望的那样。

理论上可行(但在C#中仍然无效)的情况:

if(true)
{
    string var = "VAR";
}

string var = "New VAR!";

而且这是不可接受的(因为它会隐藏父变量):

string var = "New VAR!";

if(true)
{
    string var = "VAR";
}

在变量和作用域方面,这两种情况都被精确地处理。

现在,如果在这种情况下,您不能只给其中一个变量指定不同的名称,是否有任何实际原因呢?我假设(希望)您实际的变量没有被称为var,所以我真的看不出这是个问题。如果您仍然想重复使用相同的变量名称,只需将它们放在兄弟级别作用域中:

if(true)
{
    string var = "VAR";
}

{
    string var = "New VAR!";
}

然而,虽然这在编译器中是有效的,但在阅读代码时可能会导致一定程度上的困惑,因此我几乎不建议使用它。


当然,编译器将能够区分这两个作用域,内部变量只会隐藏外部变量,并使其无法访问。 - Mats Fredriksson
3
没错,这是不被允许的,因为它可能会导致混淆或被认为是糟糕的设计实践。 - Noldorin
从IL的角度来看,我非常确定if{}作用域不存在,这意味着编译器需要为潜在的混淆值做出一些技巧,所以如果我正确的话,它并没有被积极禁止,而是被动地未实现所需的功能。 - Rune FS
4
顺便提一下,你在一个已删除的帖子评论中说条件语句会创建一个作用域,但实际上并不会。是会创建一个作用域。许多语句确实会创建作用域--例如for、foreach、using、try/catch/finally等等。但条件语句不是其中之一;创建作用域的是块,而不是条件语句。 - Eric Lippert
@Eric:相信你会指出这样的问题。:) 当然,你是正确的,尽管这是一个非常微妙的点。实际上,花括号表示块,而你指出的那些显著的例外则表示块。如果我错了,请纠正我,但是只有一行操作的if语句不包含子范围,对吗? - Noldorin

34
这难道不是错误的吗?
不,这个并没有错。这是C#规范7.5.2.1章节“块中简单名称的不变含义”正确的实现。
该规范声明:
对于在表达式或声明符中作为简单名称出现的给定标识符的每个出现, 在该出现的局部变量声明空间内,同一标识符作为简单名称的每个其他出现都必须引用相同的实体。 这个规则确保了一个名称在一个给定的块、switch块、 for-, foreach- or using-statement, 或匿名函数内始终具有相同的意义。
为什么C#无法区分两个范围?
这个问题是无意义的;显然编译器能够区分两个范围。 如果编译器不能区分两个范围,那么怎么会产生错误呢?错误消息说明存在两个不同的范围,因此已经区分了这两个范围。
第一个IF的范围为什么不应该完全与方法的其余部分分开?
不,不应该这样做。条件语句结果中块语句定义的范围(和局部变量声明空间)在词汇上是方法主体所定义的外层块的一部分。因此,适用于外层块内容的规则也适用于内层块的内容。
我无法从if之外调用var,因此错误消息是错误的,因为第一个var和第二个范围无关。
这是完全错误的。仅仅因为局部变量不再在范围内,就得出外层块不包含错误的结论是不成立的。错误消息是正确的。
这里的错误与任何变量的范围是否重叠无关;唯一相关的是在一个块中使用相同的简单名称来引用两个完全不同的东西。C#要求一个简单名称在首次使用它的块中具有一个含义。例如:
class C 
{
    int x;
    void M()
    { 
        int x = 123;
    }
}

那是完全合法的;外部x的范围与内部x的范围重叠,但这不是一个错误。错误的是:

class C 
{
    int x;
    void M()
    { 
        Console.WriteLine(x);
        if (whatever)
        {
            int x = 123;
        }
    }
}

因为现在"X"这个简单名称在M的函数体内有两种不同的含义——它既表示"this.x",又表示本地变量"x"。在同一个块中使用相同的简单名称表示两个完全不同的事情,这会让开发人员和代码维护者感到困惑,所以这是非法的。

我们允许并行块包含以两种不同方式使用的相同简单名称;这是合法的:

class C 
{
    int x;
    void M()
    { 
        if (whatever)
        {
            Console.WriteLine(x);
        }
        if (somethingelse)
        {
            int x = 123;
        }
    }
}

因为现在唯一包含两个不一致使用 x 的块是外层块,但是该块没有 直接 包含任何 "x" 的用法,只有间接地使用


@Eric:我觉得,除了其他事情之外,http://blogs.msdn.com/ericlippert/archive/2009/08/03/what-s-the-difference-part-two-scope-vs-declaration-space-vs-lifetime.aspx 在这里也是相关的。 - Brian
不错的观点;虽然实际上,我们讨论的是块作为本地变量声明空间还是块作为本地变量范围的问题,对于这个问题来说几乎是无关紧要的。问题 似乎 是关于声明空间的,因为声明被标记了。但实际上它是关于 范围 的 - 我们有两个实体,它们都通过其未限定名称成功查找,并绑定到同一块中的不同实体。 - Eric Lippert
3
在同一代码块中访问具有相同名称的字段和局部变量的示例,可能是解释这个设计决策的最具启发性的单个例子。 - Pavel Minaev
1
可能只是我个人感觉,但你回答的总体语气似乎有点激烈。 - Philip Wallace
@PhilipWallace:你可能会对阅读我关于这个主题的文章感兴趣:http://blogs.msdn.com/b/ericlippert/archive/2008/02/20/how-to-not-get-a-question-answered.aspx - Eric Lippert

11
这在C++中是合法的,但会引发许多错误和不眠之夜。我认为,C#开发人员决定发出警告/错误信息,因为在绝大多数情况下,这是一个错误,而不是程序员实际想要的东西。这里有一个有趣的讨论,讨论了规范中的哪些部分出现了这个错误。
编辑(一些示例)-----
在C++中,以下代码是有效的(外部声明在内部作用域之前或之后并不重要,如果在之前更有意思且容易出错)。
void foo(int a)
{
    int count = 0;
    for(int i = 0; i < a; ++i)
    {
        int count *= i;
    }
    return count;
}

现在想象一下,如果这个函数更长了几行,可能很容易就看不出错误。编译器从来不会抱怨(以前是这样的,不确定新版本的C++是否仍然如此),而函数总是返回0。

这个行为显然是一个bug,因此最好能够让c++-lint程序或编译器指出这一点。如果它不是一个bug,则可以通过重命名内部变量来轻松解决。

更令人恼火的是,我记得GCC和VS6对于for循环中的计数器变量属于哪个作用域有不同的观点。其中一个认为它属于外部作用域,另一个则认为它不属于外部作用域。这对于跨平台代码的开发而言有点烦人。让我再给你举个例子,以保持我的字数。

for(int i = 0; i < 1000; ++i)
{
    if(array[i] > 100)
        break;
}

printf("The first very large value in the array exists at %d\n", i);

我记得这段代码在VS6中能够工作,但在GCC中却不能。不管怎样,C#对一些东西进行了清理,这是好事。


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