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

白昼怎懂夜的黑 提交于 2020-05-07 21:42:59

问题


What I am showing below, is rather a theoretical question. But I am interested in how the new C#7 compiler works and resolves local functions.

In C#7 I can use local functions. For example (you can try these examples in LinqPad beta):

Example 1: Nested Main()

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

DotNetFiddle for Example 1

Rather than calling Main() in a recursive way, the local function Main() is being called once, so the output of this is:

Hello!

The compiler accepts this without warnings and errors.

Example 2: Here, I am going one level deeper, like:

DotNetFiddle for Example 2

In this case, I would also expect the same output, because the innermost local function is called, then one level up, Main() is just another local function with a local scope, so it should be not much different from the first example.

But here, to my surprise, I am getting an 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


Question: Can you explain why this error happens in Example 2, but not in Example 1?

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


Update: Thank you to all who have made contibutions so far (either answers or comments), it is very worthwile what you wrote to understand the behavior of the C# compiler.

From what I read, and after considering the possibilities, what I found out with your help is that it can be either a compiler bug or behavior by design.

Recall that C# had some design goals which differentiate it from languages like C++.

If you're interested what I have done to investigate it further: I have renamed the innermost function to MainL like:

Example 2b:

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

This modified example compiles and runs successfully.

Now when you compile this with LinqPad and then switch to the IL tab you can see what the compiler did:

It created the innermost MainL function as g__MainL0_1, the enclosing Main function has the label g__Main0_0.

That means, if you remove the L from MainL you will notice that the compiler already renames it in a unique way, because then the code looks like:

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         

which would still resolve correctly. Since the code doesn't look like this in Example 2, because the compiler stops with an error, I do now assume that the behavior is by design, it is not likely a compiler bug.

Conclusion: Some of you wrote that in C++ recursive resolution of local functions can lead to refactoring issues, and others wrote that this kind of behavior in C# is what the compiler does with local variables (note that the error message is the same) - all that even confirms me thinking it was done like this by design and is no bug.


回答1:


Parameters and local variables from the enclosing scope are available inside a local function.

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

C# does not overwrite names from the parent scope, so there is and ambiguity for the local name Main which defined in current and parent scopes.

So in the second example both declaration of void Main() are available for the inner scope and compiler shows you an error.

Here is an example with variables and local functions which could help you to see the problem in the familiar environment. To make it clear that it is only matter of scope I modified example and added functions to variables to make it clear:

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}");
    }
}

There are even the same errors when you try to overwrite local variable and local function.

So as you can see it is a matter of scopes and you cannot overwrite local function or variable.

BTW, in a first example you could make recursive call by using this.Main();:

void Main()
{
    void Main()
    {
        Console.WriteLine("Hello!");
    }
    this.Main(); // call instance method
}

Footnote: Local functions are not represented as delegates as some commentators suggest and it makes local functions much leaner in both memory and CPU.




回答2:


To expand a bit on v-andrew's answer, it is indeed analogous to having two variables with the same name. Consider that the following is allowed:

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

Here we've two scopes and so we can have two local functions of the same name in the same method.

Also to combine v-andrew's answer and your question, note that you can (and always could) have a variable called Main inside Main() but you can't have both a variable and a local function of the same name in the same scope either.

On the other hand, you can't overload locals like you can members by having different parameters.

Really, it's all closer to the existing rules for locals than the existing rules for methods. Indeed, it's the same rules. Consider you can't do:

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.

It is, but the scope includes the name of the local function. C.f. that you can't redefine a variable name from a for, foreach or using inside the its scope.

Meanwhile, I think it is a compiler bug.

It's a compiler feature.

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.

That means it's possible to introduce a bug into the compiler where the code you have in your question would work. That would be in violation to the C# rules for names of locals.

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

It blocks something that has been known as a source of mistakes for some time. Likewise in C# you are not allowed to use integer values with if() and you have to explicitly fall-through in switch statements. All of these are changes that C# made to how it compares to C++ in the very beginning and all of them remove some convenience, but all of them are things that people really had found caused bugs and often prohibited in coding conventions.




回答3:


Since Stackoverflow does not allow multiple answers, I thought what would be the fairest way. I created this answer as community wiki (so I won't get any rep points for this answer), upvoted the two answers below and added them as link for your reference (so they are honored and get rep points for their answers individually):

  • Answer 1: v-andrew
  • Answer 2: Jon Hanna

And I created a summary in the question containing all information I got from you from the comments and from the answers:

  • Summary: in the question



回答4:


As c# is a statically compiled language, i'd think all the functions are compiled before their encompassing scope is executed and therefor the innermost Main cannot be declared because a Main already exists from its' closure point of view (one level up).

Note that this is not based on factual evidence but just my initial thoughts on the matter.

After some research... I was wrong, mostly. My gut feeling does seem to imply the same behaviour, But not for the reasons I initially thought.

@PetSerAl already explained in comment form better than I could have copied it from manuals so I defer to that answer.



来源:https://stackoverflow.com/questions/41288668/why-is-a-local-function-not-always-hidden-in-c7

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!