为什么局部函数并不总是隐藏在 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();
}
不是以递归方式调用 Main()
,而是调用局部函数 Main()
一次,因此输出为:
Hello!
编译器在没有警告和错误的情况下接受它。
示例 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。您不能在其范围内从 for
、foreach
或 using
重新定义变量名称。
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:
我在问题中创建了一个摘要,其中包含我从您的评论和答案中获得的所有信息:
- 总结:
我在下面展示的是一个理论问题。但我对新的 C#7 编译器如何工作和解析本地函数很感兴趣。
在C#7中我可以使用局部函数。例如(你可以在LinqPad beta中尝试这些例子) :
示例 1: 嵌套 Main()
void Main()
{
void Main()
{
Console.WriteLine("Hello!");
}
Main();
}
不是以递归方式调用 Main()
,而是调用局部函数 Main()
一次,因此输出为:
Hello!
编译器在没有警告和错误的情况下接受它。
示例 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。您不能在其范围内从 for
、foreach
或 using
重新定义变量名称。
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:
我在问题中创建了一个摘要,其中包含我从您的评论和答案中获得的所有信息:
- 总结: