为什么局部函数并不总是隐藏在 C#7 中?

Why is a local function not always hidden in C#7?

我在下面展示的是一个理论问题。但我对新的 C#7 编译器如何工作和解析本地函数很感兴趣。

C#7中我可以使用局部函数。例如(你可以在LinqPad beta中尝试这些例子) :

示例 1: 嵌套 Main()

void Main()
{
    void Main()
    {
        Console.WriteLine("Hello!");
    }
    Main();
}

DotNetFiddle for Example 1

不是以递归方式调用 Main(),而是调用局部函数 Main() 一次,因此输出为:

Hello!

编译器在没有警告和错误的情况下接受它。

示例 2: 在这里,我要更深入一层,例如:

DotNetFiddle for Example 2

在这种情况下,我也希望得到相同的输出,因为最里面的局部函数被调用,然后向上一层,Main() 只是另一个具有局部作用域的局部函数,所以它应该不是与第一个示例有很大不同。

但在这里,令我惊讶的是,我收到了一个错误:

CS0136 A local or parameter named 'Main' cannot be declared in this scope because that name is used in an enclosing local scope to define a local or parameter


问题: 您能解释一下为什么在示例 2 中会出现此错误,而在示例 1 中 不会 吗?

我想,每个内部 Main() 都会有一个局部作用域并且隐藏在外部。


更新: 感谢到目前为止所有做出贡献的人(无论是答案还是评论),您所写的内容对于理解 C# 编译器的行为非常有价值。

根据我阅读的内容以及考虑各种可能性后,我在您的帮助下发现它可能是编译器错误或设计行为。

剧透:

我们得出的结论是这是一个设计选择,而不是错误。

回想一下,C# 有一些与 C++ 等语言不同的设计目标。

如果您对我为进一步调查所做的工作感兴趣: 我已将最里面的函数重命名为 MainL,例如:

示例 2b:

void Main()
{
    void Main()
    {
        void MainL()
        {
            Console.WriteLine("Hello!");
        }
        MainL();
    }
    Main();     
}

修改后的示例编译运行成功

现在,当您使用 LinqPad 编译它然后切换到 IL 选项卡时,您可以看到编译器做了什么:

它创建了最内层的 MainL 函数作为 g__MainL0_1,封闭的 Main 函数具有标签 g__Main0_0.

这意味着,如果您从 MainL 中删除 L,您会注意到编译器已经以独特的方式重命名了它,因为这样代码看起来像:

IL_0000:  call        UserQuery.<Main>g__Main0_0
IL_0005:  ret         

<Main>g__Main0_0:
IL_0000:  call        UserQuery.<Main>g__Main0_1
IL_0005:  ret         

<Main>g__Main0_1:
IL_0000:  ldstr       "Hello!"
IL_0005:  call        System.Console.WriteLine
IL_000A:  ret         

仍然可以正确解析。由于代码在示例 2 中看起来不像这样,因为编译器因错误而停止,我现在假设该行为是设计使然,它不太可能是编译器错误。

结论: 你们中的一些人写道,在 C++ 中,局部函数的递归解析会导致重构问题,而另一些人则写道,C# 中的这种行为是编译器对局部变量所做的(注意错误消息是相同的)——甚至证实我认为这是设计使然,没有错误。

由于 c# 是一种静态编译语言,我认为所有函数都是在执行其包含范围之前编译的,因此无法声明最里面的 Main,因为 Main 已经存在于它的关闭观点(向上一级)。

请注意,这并非基于事实证据,而只是我对此事的初步想法。

经过一些研究...我错了,大部分。我的直觉似乎暗示了相同的行为,但不是出于我最初认为的原因。

@PetSerAl 已经以评论形式解释得比我从手册中复制的更好,所以我尊重那个答案。

封闭范围内的参数和局部变量在局部函数内可用。

I thought, each inner Main() would have a local scope and is hidden outside.

C# 不会覆盖父作用域中的名称,因此在当前作用域和父作用域中定义的局部名称 Main 存在歧义。

因此在第二个示例中,void Main() 的两个声明都可用于内部作用域,编译器会向您显示错误。

这是一个带有变量的示例,local functions 可以帮助您在熟悉的环境中查看问题。为了清楚地表明这只是范围问题,我修改了示例并向变量添加了函数以使其清楚:

class Test
{
    int MainVar = 0;
    public void Main()
    {
        if (this.MainVar++ > 10) return;
        int MainVar = 10;
        Console.WriteLine($"Instance Main, this.MainVar=${this.MainVar}, MainVar={MainVar}");
        void Main()
        {
            if (MainVar++ > 14) return;
            Console.WriteLine($"Local Main, this.MainVar=${this.MainVar}, MainVar={MainVar}");
            // Here is a recursion you were looking for, in Example 1
            this.Main();
            // Let's try some errors!
            int MainVar = 110; /* Error! Local MainVar is already declared in a parent scope. 
                //  Error CS0136  A local or parameter named 'MainVar' cannot be declared in this scope 
                // because that name is used in an enclosing local scope to define a local or parameter */
            void Main() { } /* Error! The same problem with Main available on the parent scope. 
                // Error CS0136  A local or parameter named 'Main' cannot be declared in this scope 
                // because that name is used in an enclosing local scope to define a local or parameter */
        }
        Main(); // Local Main()
        this.Main(); // Instance Main()
        // You can have another instance method with a different parameters
        this.Main(99);
        // But you can't have a local function with the same name and parameters do not matter
        void Main(int y) { } // Error! Error CS0128  A local variable or function named 'Main' is already defined in this scope
    }
    void Main(int x)
    {
        Console.WriteLine($"Another Main but with a different parameter x={x}");
    }
}

当您尝试覆盖局部变量和局部函数时,甚至会出现相同的错误。

如您所见,这是一个作用域问题,您不能覆盖局部函数或变量。

顺便说一句,在第一个示例中,您可以使用 this.Main();:

进行递归调用
void Main()
{
    void Main()
    {
        Console.WriteLine("Hello!");
    }
    this.Main(); // call instance method
}

脚注:局部函数并不像某些评论员所建议的那样表示为委托,这使得 local functions 在内存和 CPU 方面都更加精简。

为了扩展 v-andrew 的答案,它确实类似于具有相同名称的两个变量。考虑以下 允许的:

void Main()
{
    {
        void Main()
        {
            Console.WriteLine("Hello!");
        }
        Main();
    }
    {
        void Main()
        {
            Console.WriteLine("GoodBye!");
        }
        Main();
    }
}

这里我们有两个作用域,所以我们可以在同一个方法中有两个同名的局部函数。

还要结合 v-andrew 的答案和你的问题,请注意你可以(并且总是可以)在 Main() 中有一个名为 Main 的变量,但你不能同时拥有一个变量和在同一范围内的同名局部函数。

另一方面,您不能像成员一样通过使用不同的参数来重载局部变量。

真的,与方法的现有规则相比,它更接近本地的现有规则。确实,这是相同的规则。考虑到你不能这样做:

void Main()
{
    {
        void Main()
        {
            int Main = 3;
            Console.WriteLine(Main);
        }
        Main();
    }
}

I thought, each inner Main() would have a local scope and is hidden outside.

是的,但是范围包括本地函数的名称。 C.f。您不能在其范围内从 forforeachusing 重新定义变量名称。

Meanwhile, I think it is a compiler bug.

这是一个编译器功能。

That means, removing the L from MainL should not harm because the compiler already renames it in a unique way, it should result in IL code like.

这意味着可能会在编译器中引入一个错误,而您在问题中拥有的代码可以正常工作。这将违反当地人姓名的 C# 规则。

it is confusing in C#, but logical in C++

它阻止了一段时间以来被认为是错误来源的东西。同样,在 C# 中,您不允许将整数值与 if() 一起使用,并且您必须在 switch 语句中显式失败。所有这些都是 C# 在一开始就相对于 C++ 所做的更改,所有这些都删除了一些便利,但所有这些都是人们真正发现的导致错误的东西,并且在编码约定中经常被禁止。

由于 Whosebug 不允许多个答案,所以我认为最公平的方法是什么。我将此答案创建为社区维基(因此我不会为此答案获得任何代表积分),对下面的两个答案投了赞成票并将它们添加为 link 以供您参考(因此他们很荣幸并获得代表积分单独回答):

  • 答案 1:
  • 答案 2:

我在问题中创建了一个摘要,其中包含我从您的评论和答案中获得的所有信息:

  • 总结: