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

Mat*_*att 20 c# scope nested function c#-7.0

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

C#7中,我可以使用本地函数.例如(您可以在LinqPad beta中尝试这些示例):

示例1:嵌套Main()

void Main()
{
    void Main()
    {
        Console.WriteLine("Hello!");
    }
    Main();
}
Run Code Online (Sandbox Code Playgroud)

而不Main()是以递归方式调用,本地函数Main()被调用一次,因此它的输出是:

你好!

编译器接受此操作时没有警告和错误.

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

void Main()
{
    void Main()
    {
        void MainL()
        {
            Console.WriteLine("Hello!");
        }
        MainL();
    }
    Main();     
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,我也期望相同的输出,因为最里面的本地函数被调用,然后一个级别,Main()只是另一个具有局部范围的本地函数,所以它应该与第一个示例没有太大的不同.

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

CS0136无法在此范围内声明名为"Main"的本地或参数,因为该名称用于封闭的本地范围以定义本地或参数


问题: 您能解释为什么在例2中发生此错误,但在示例1中没有?

我想,每个内部Main()都有一个局部范围,并隐藏在外面.


更新: 感谢所有到目前为止已经提出建议的人(无论是答案还是评论),你为了解C#编译器的行为而写的是非常值得的.

根据我的阅读,在考虑了可能性之后,我在你的帮助下发现它可能是编译器错误或设计行为.

回想一下,C#有一些设计目标,可以将它与C++等语言区分开来.

如果你对我所做的事情有兴趣进一步研究它:我已经将最里面的函数重命名为MainL:

例2b:

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         
Run Code Online (Sandbox Code Playgroud)

此修改示例编译并成功运行.

现在,当您使用LinqPad编译它,然后切换到IL选项卡时,您可以看到编译器执行的操作:

标签

它创建了最里面的MainL功能g__MainL0_1,封闭Main功能有标签g__Main0_0.

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

void Main()
{
    void Main()
    {
        Console.WriteLine("Hello!");
    }
    Main();
}
Run Code Online (Sandbox Code Playgroud)

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

结论: 有些人写道,在C++中,本地函数的递归解析会导致重构问题,而其他人写道,C#中的这种行为是编译器对局部变量的作用(请注意错误消息是相同的) - 所有这甚至证实了我认为它是按照设计完成的,并且没有错误.

v-a*_*rew 11

封闭范围内的参数和局部变量在本地函数中可用.

我想,每个内部Main()都有一个局部范围,并隐藏在外面.

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}");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

因此,您可以看到它是范围问题,您不能覆盖本地函数或变量.

顺便说一下,在第一个例子中,您可以使用this.Main();以下命令进行递归调用:

void Main()
{
    void Main()
    {
        Console.WriteLine("Hello!");
    }
    this.Main(); // call instance method
}
Run Code Online (Sandbox Code Playgroud)

脚注:本地函数不像一些评论员所建议的那样表示为委托,它local functions在内存和CPU方面都更加精简.

  • 外部`Main`是一个类方法,其中本地`Main`是本地的,因此它可以覆盖当前范围的全局`Main`.这是一个原因,为什么在第一个例子中执行本地`Main` (2认同)

Jon*_*nna 7

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

void Main()
{
    {
        void Main()
        {
            Console.WriteLine("Hello!");
        }
        Main();
    }
    {
        void Main()
        {
            Console.WriteLine("GoodBye!");
        }
        Main();
    }
}
Run Code Online (Sandbox Code Playgroud)

这里我们有两个范围,因此我们可以在同一个方法中有两个同名的本地函数.

另外要结合v-andrew的答案和你的问题,请注意你可以(并且总是可以)拥有一个名为Maininside 的变量,Main()但你不能在同一范围内同时具有同名的变量和本地函数.

另一方面,您不能通过使用不同的参数来过载本地成员,就像您可以成员一样.

实际上,它比现有的方法规则更接近现有的本地规则.实际上,这是相同的规则.考虑你做不到:

void Main()
{
    {
        void Main()
        {
            int Main = 3;
            Console.WriteLine(Main);
        }
        Main();
    }
}
Run Code Online (Sandbox Code Playgroud)

我想,每个内部Main()都有一个局部范围,并隐藏在外面.

它是,但范围包括本地函数的名称.比照,你不能从一个重新定义一个变量名for,foreachusing在其范围内.

同时,我认为这是一个编译器错误.

这是一个编译器功能.

这意味着,从MainL中删除L不应该受到伤害,因为编译器已经以一种独特的方式重命名它,它应该导致IL代码.

这意味着可以在编译器中引入一个错误,您的问题中的代码可以使用.这违反了本地人姓名的C#规则.

它在C#中令人困惑,但在C++中是合乎逻辑的

它阻止了一段时间以来被称为错误来源的东西.同样在C#中,不允许使用整数值,if()并且必须在switch语句中明确地使用.所有这些都是C#在一开始就对C++进行比较所做的改变,并且所有这些改变都消除了一些便利,但所有这些都是人们真正发现的导致错误并且通常在编码约定中被禁止的东西.


Mat*_*att -2

由于 Stackoverflow 不允许多个答案,我想什么是最公平的方法。我将这个答案创建为社区维基(因此我不会获得该答案的任何代表点),对下面的两个答案进行了投票,并将它们添加为链接以供您参考(因此他们很荣幸并单独获得其答案的代表点):

  • 答案 1:v-andrew(点击链接并投票)
  • 答案 2:Jon Hanna(点击链接并点赞)

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