使用C#编译器时出现变量名称歧义的奇怪问题

12

让我们来看下面的代码:

class Foo
{
   string bar;

   public void Method()
   {
      if (!String.IsNullOrEmpty(this.bar))
      {
         string bar = "Hello";
         Console.Write(bar);
      }
   }
}

这将编译通过,一切都很好。然而,现在让我们去掉 this. 前缀:

class Foo
{
   string bar;

   public void Method()
   {
      if (!String.IsNullOrEmpty(bar)) // <-- Removed "this."
      {
         string bar = "Hello";
         Console.Write(bar);
      }
   }
}
在这种情况下,我收到编译器错误。我同意这是一个错误,然而让我困惑的是错误发生在以下行上:
string bar = "Hello";

带有以下信息:

无法在此作用域中声明名为“bar”的局部变量,因为它将赋予“bar”不同的含义,而“bar”已经在“父级或当前”作用域中用于表示其他内容。

从我对编译器的理解来看,bar 的声明会被提升到 Method() 方法的顶部。但是,如果是这种情况,则下面这行代码:

if (!String.IsNullOrEmpty(bar))

应该考虑到歧义问题,因为bar可能是对实例字段的引用,也可能是对尚未声明的局部变量的引用。

我认为,删除this.会导致另一行编译错误似乎很奇怪。换句话说,在作用域中声明本地变量bar是完全有效的,只要之前没有进行过潜在的歧义引用bar(请注意,如果我注释掉if (!String.IsNullOrEmpty(bar)),则错误将消失)。

这听起来有点吹毛求疵,那么你的问题是什么?

我的问题是,为什么编译器允许在作用域内先声明一个模棱两可的变量引用,但随后标记声明本身为冗余。难道不应该将String.IsNullOrEmpty()中对bar的模棱两可引用作为更精确的错误位置吗?在我的示例中,当然很容易发现,但当我在实际工作中遇到此问题时,引用距离很远,更难以追踪。


看起来它已经在类级别声明了,这似乎是一个正常的编译器错误,你正在if CodeBlock内重新声明字符串bar =“Hello”; 这只是一个简单的作用域问题。 - MethodMan
@DJKRAZE:这并不能解释为什么在第一个引用中添加this.会使第二个引用编译! - Gabe
再一次,C#的愚蠢“词法”作用域... - leppie
1
如果你想让Eric Lippert发表评论,他在这个主题上有一篇博客文章 ;) 链接 - Servy
是的,我不认为这是一个作用域问题(我对作用域的理解很好)。Jon通过语言规范澄清了这一点-一旦使用名称,每个相同标识符的其他出现必须引用相同的实体。然而,它基本上是“先到先得”的方法。我第一次引用bar时,所有未来的引用都必须引用那个实体。因此,局部变量bar的新声明是一种违规行为。很有道理。 - Mike Christensen
2个回答

11
据我了解编译器,bar的声明被提升到Method()方法的顶部。
不是这样的情况。
错误信息非常精确:
在此范围内无法声明名为'bar'的局部变量,因为它会赋予'bar'不同的含义,而'bar'已在“父级或当前”范围内用于表示其他内容。
违反了C#规范的第7.6.2.1节(C# 4和5规范)。
对于在表达式或声明符中作为完整简单名称(没有类型参数列表)出现的给定标识符的每个出现,在立即包围该出现的局部变量声明空间(§3.3)内,作为完整简单名称的相同标识符的每个其他出现都必须引用相同的实体。此规则确保名称的含义始终在给定块、开关块、for、foreach或using语句或匿名函数内保持一致。
此规则的另一个更微妙的理想后果之一是,进行涉及移动局部变量声明的重构变得更加安全。任何此类重构都将导致简单名称改变其语义,编译器都会捕获到。
除此之外,我认为这只是有利于澄清。即使允许第二种版本,我认为第一种更清晰。编译器确保您不编写病态不清晰的代码,而您可以非常容易地将其修复为明确您的意图。
换句话说:您真的想能够编写第二个版本吗?
特别是:在我的例子中,当然很容易发现,但是当我在真实场景中遇到这个问题时,引用已经在前几页了,追踪起来更加困难。
这样做使得允许它变得更加合理吗?相反,我认为应该将其视为强烈的鼓励,重构您的代码,使单个方法不再“长达数页”。

谢谢Jon!这实际上是我希望得到的确切答案,还参考了语言规范中澄清此行为的适当位置。我以前从未遇到过去除this.前缀会导致程序其他地方出现新的编译器错误的情况,并希望获得有关设计决策背后的见解。 - Mike Christensen
5
我其实不明白为什么StackOverflow会允许你给Jon Skeet的答案点踩。请帮我翻译这句话。 - Mike Christensen

1

bar的第二个定义没有被拉到方法级别,它的作用域是if块。例如,这是完全有效的。

class Foo
{
    private string bar;

    public void Method()
    {
        if (!String.IsNullOrEmpty(bar)) // <-- No more this.
        {
            string bar1 = "Hello";
            Console.Write(bar);
        }

        if (!String.IsNullOrEmpty(bar)) // <-- No more this.
        {
            string bar1 = "Hello";
            Console.Write(bar);
        }
    }
}

原因是bar不再含糊,外部和内部作用域之间有明确的名称区分:bar/bar1 - 编译器禁止通过本地定义覆盖外部作用域的bar

并没有完全回答这个问题。 - gdoron
1
@gdoron,他在那里有几个问题。其中一点他说他同意这是一个错误,但不同意错误发生的位置,因为他认为bar的第二个声明被拉到了Method()的顶部。 - taylorjonl
我没有给它点踩,嗯,可以说它回答了问题的一部分。 :) - gdoron
@gdoron,同意不同意,问题已经被编辑并在最后一段总结了他的问题,我认为我的答案非常准确。他的困惑不在于是否存在错误,而是由于他没有理解变量的作用域而不知道错误应该出现在哪里。 - taylorjonl

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