语句块中的变量作用域

12
for (int i = 0; i < 10; i++)
{
    Foo();
}
int i = 10; // error, 'i' already exists

----------------------------------------    

for (int i = 0; i < 10; i++)
{
    Foo();
}
i = 10; // error, 'i' doesn't exist

根据我的作用域理解,第一个例子应该没问题。两个例子都不允许似乎更奇怪。毕竟,'i' 只能在范围内或不在范围内。

是否有我不理解的关于作用域的非明显信息使编译器无法真正解决这个问题?还是只是编译器的保姆状态?

9个回答

17
根据我的 scope 理解,第一个例子应该没问题。
你的 scope 理解正确。这不是作用域错误,而是简单名称错误的不一致使用。
int i = 10; // 错误:“i”已经存在
那不是报告的错误。报告的错误是“无法在此范围内声明名为 i 的局部变量,因为这会赋予 i 不同的含义,在子范围中已经用于表示其他内容”。
错误消息告诉您错误在哪里,再次阅读错误消息。它没有说明声明之间有冲突,而是说错误是因为改变了简单名称的含义。出错的原因不是重复声明;即使这些作用域嵌套,并且在两个不同的作用域中有两个具有相同名称的元素是合法的。不合法的是在嵌套的本地变量声明空间中使一个简单名称意味着两个不同的东西。
如果您像下面这样做,就会收到“在此范围内已经定义了名为 i 的局部变量”的错误:
int i = 10;
int i = 10;

当然,“i”要么在作用域内,要么不在。

是的,但这有什么关系呢?一个给定的“i”是否在作用域内是无关紧要的。例如:

class C 
{
    int i;
    void M()
    {
        string i;

完全合法。在整个 M 函数中,外部 i 变量都是可见的。在局部声明一个同名的变量并不会有问题。如果你这样做:

class C 
{
    int i;
    void M()
    {
        int x = i;
        foreach(char i in ...

因为现在你在两个嵌套的局部变量声明空间中使用了i来代表两个不同的东西--一个循环变量和一个字段。这样会让人混淆并容易出错,因此我们将其禁止。

我是否对作用域有一些不明显的理解,导致编译器无法真正地解决这个问题?

我不理解这个问题。显然编译器能够完全分析程序;如果编译器不能解决每个i的含义,那么它怎么能报告错误消息呢? 编译器完全能够确定你在同一个局部变量声明空间中使用了“i”表示两个不同的东西,并相应地报告错误。


11

这是因为声明空间在方法级别定义了i。变量i在循环结束时超出了作用域,但仍然不能重新声明i,因为i已经在该方法中定义过了。

作用域 vs 声明空间:

http://csharpfeeds.com/post/11730/Whats_The_Difference_Part_Two_Scope_vs_Declaration_Space_vs_Lifetime.aspx

你需要看看Eric Lippert的答案(他总是正确的,特别是在这些问题上)。

http://blogs.msdn.com/ericlippert/archive/2009/08/03/what-s-the-difference-part-two-scope-vs-declaration-space-vs-lifetime.aspx

以下是Eric在上述帖子中的评论,我认为它解释了他们为什么这样做:

这样看吧。只要在同一个块中保持不变,将变量声明向上移动在源代码中应该总是合法的,对吧?如果我们按照您的建议这样做,那么有时将是合法的,有时将是不合法的!但我们真正想避免的是C++中发生的事情——在C++中,有时移动变量声明实际上会更改其他简单名称的绑定!


1
但是为什么你可以在同一个方法的另一个循环的作用域中定义 i 呢?我仍然认为这只是编译器方面的官僚主义。 - Michael Meadows
1
@Michael:你可以这样做,因为循环是兄弟作用域。在兄弟作用域中定义具有相同名称的变量不会发生冲突,因为它们彼此之间是超出范围的。 - Zach Johnson
真的吗?我喜欢这样。我想不出在同一个方法中重新声明具有相同名称的变量的理由,这可以防止我(或其他人)将变量移动并在未来引起意外行为。 - kemiller2002
2
感谢提及。有几点需要注意。首先,这篇文章更好:http://blogs.msdn.com/ericlippert/archive/2009/11/02/simple-names-are-not-so-simple.aspx;其次,最好链接到原始文章而不是副本,这样您就可以获取我的更新并阅读其他人的评论:http://blogs.msdn.com/ericlippert/archive/2009/08/03/what-s-the-difference-part-two-scope-vs-declaration-space-vs-lifetime.aspx。 - Eric Lippert
@Zach:好吧,这个答案(以及链接的文档)说得不一样:它们在不同的声明空间中。但是从作用域的角度来看,两者都可以存在,因为它们都是该方法的子级(不重叠)。i循环在循环结束时超出范围! - Foxfire
显示剩余7条评论

5

根据C#规范中的局部变量声明:

在局部变量声明中声明的局部变量的作用域是声明出现的块。

当然,在声明之前不能使用i,但i声明的作用域是包含它的整个块:

{
    // scope starts here
    for (int i = 0; i < 10; i++)
    {
        Foo();
    }
    int i = 10;
}
for循环中的i变量处于子作用域,因此会出现变量名冲突。
如果我们重新排列声明的位置,冲突将更加明显:
{
    int i = 10;

    // collision with i
    for (int i = 0; i < 10; i++)
    {
        Foo();
    }
}

2

是的,我赞同“保姆式编译器主义”的评论。有趣的是,这是可以接受的。

for (int i = 0; i < 10; i++)
{

}

for (int i = 0; i < 10; i++)
{

}

并且这是可以的

for (int i = 0; i < 10; i++)
{

}

for (int j = 0; j < 10; j++)
{
    var i = 12;                
}

但这并不是。
for (int i = 0; i < 10; i++)
{
    var x = 2;
}

var x = 5;

即使您可以这样做
for (int i = 0; i < 10; i++)
{
    var k = 12;
}

for (int i = 0; i < 10; i++)
{
    var k = 13;
}

这一切都有点不太一致。

编辑

根据下面与Eric的评论交流,我认为展示如何处理循环可能会有所帮助。 尽可能将循环组合成自己的方法。 我这样做是因为它有助于提高可读性。

之前

/*
 * doing two different things with the same name is unclear
 */
for (var index = 0; index < people.Count; index++)
{
    people[index].Email = null;
}
var index = GetIndexForSomethingElse(); 

之后

/*
 * Now there is only one meaning for index in this scope
 */
ClearEmailAddressesFor(people); // the method name works like a comment now
var index = GetIndexForSomethingElse();

/*
 * Now index has a single meaning in the scope of this method.
 */
private void ClearEmailAddressesFor(IList<Person> people)
{
    for (var index = 0; index < people.Count; index++)
    {
        people[index].Email = null;
    }
}

1
这并不矛盾。规则是在直接使用该简单名称的最外层局部变量声明空间中,相同的简单名称不能用于表示两个不同的事物。for循环定义了一个局部变量声明空间。我认为您会发现,在每个示例中都一致地应用了此规则。 - Eric Lippert
1
当你解析规范的语言时,它可能是一致的,但从消费者的角度来看,它是不一致的。如果一个变量超出了作用域并且无法使用,那么我没有理由不能创建另一个同名变量。 - Michael Meadows
有一个非常好的理由。这个非常好的理由是因为在重叠的声明空间中使用相同的简单名称来引用两个不同的实体是令人困惑和容易出错的,因此应该是非法的。 - Eric Lippert
1
@Eric 我认为你对此的看法受到了你熟悉内部情况的影响。从一个简单消费者的角度来看,如果我没有能力访问或修改变量,那么它就是有效范围之外的。在这种情况下,无论技术上如何处理作用域,语言都应将其视为超出范围。如果我不能在循环外创建先前在该循环中声明的变量(请参见我的第三个示例),那么这确实令人困惑,因为循环中的变量已经超出了上下文,即使不是技术上超出了范围。 - Michael Meadows
它被视为超出范围。正如我多次所说,这根本不是一个范围问题。这是一个在代码重叠区域中使用相同字母序列表示两个不同含义的问题。别再考虑范围了;首先,这不是一个作用域问题。 - Eric Lippert
好的,我犯了一个语义错误,我很抱歉。作为一名开发人员,我更愿意将for循环视为完全自主的代码块。问题可以通过将循环组合到另一个方法中来解决(后者通常更有利于提高可读性)。感谢您提供的信息性评论。 - Michael Meadows

1
在第一个例子中,循环外声明i使i成为函数的局部变量。因此,在该函数的任何块中声明另一个变量名i是错误的。
第二个例子中,i仅在循环期间处于作用域内。在循环外部,无法再访问i。
所以你已经看到了错误,但这样做没有任何问题。
for (int i = 0; i < 10; i++)
{
  // do something
}

foreach (Foo foo in foos)
{
   int i = 42;
   // do something 
}

因为变量 i 的作用域仅限于每个代码块内。


0

我认为编译器的意思是,i 在方法级别已经声明并被限定在 for 循环内部。

因此,在情况 1 中 - 你会得到一个错误,提示变量已经存在,这确实如此

& 在情况 2 中 - 由于该变量仅在 for 循环中被限定,所以无法在该循环外访问

为了避免这种情况,你可以:

var i = 0;

for(i = 0, i < 10, i++){
}

i = 10;

但我想不出有什么情况需要这样做。

希望对你有所帮助。


0

你需要做的是

            int i ;
            for ( i = 0; i < 10; i++)
            {

            }
            i = 10;

0
class Test
{
    int i;
    static int si=9; 

    public Test()
    {
        i = 199;
    }

    static void main()
    {
        for (int i = 0; i < 10; i++)
        {
            var x = 2;
        }

        { var x = 3; }

        {    // remove outer "{ }" will generate compile error
            int si = 3; int i = 0;

             Console.WriteLine(si);
             Console.WriteLine(Test.si);
             Console.WriteLine(i);
             Console.WriteLine((new Test()).i);
        }
    }
}

0
这只是一种过于保守的编译器做法吗?

确实如此。在同一方法中“重复使用”变量名称没有任何意义,只会导致错误而已。


问题不在于重复使用变量名;在一个方法中多次重复使用变量名是完全合法的。但是,在特定的局部变量声明空间内,不能使用同一“简单名称”来引用“两个不同的事物”。 - Eric Lippert

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