Почему локальная функция не всегда скрыта в С# 7?

То, что я показываю ниже, это скорее теоретический вопрос. Но меня интересует, как новый компилятор С# 7 работает и разрешает локальные функции.

В С# 7 я могу использовать локальные функции. Например (вы можете попробовать эти примеры в бета-версии LinqPad):

Пример 1: Вложенный Main()

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

DotNetFiddle для примера 1

Вместо того, чтобы вызывать Main() рекурсивным способом, локальная функция Main() вызывается один раз, поэтому результат этого:

Здравствуйте!

Компилятор принимает это без предупреждений и ошибок.

Пример 2: Здесь я иду на один уровень глубже, например:

Example2-NestedDeeper DotNetFiddle для примера 2

В этом случае я бы также ожидал того же результата, потому что вызывается самая внутренняя локальная функция, затем на один уровень выше, Main() - это просто еще одна локальная функция с локальной областью действия, поэтому она не должна сильно отличаться от первого примера.

Но здесь, к моему удивлению, я получаю ошибку:

CS0136 Локальный или параметр с именем "Main" не может быть объявлен в этой области, поскольку это имя используется во внешней локальной области для определения локального или параметра


Вопрос: Можете ли вы объяснить, почему эта ошибка происходит в Примере 2, но не в Примере 1?

Я думал, что каждый внутренний Main() будет иметь локальную область видимости и будет скрыт снаружи.


Обновление: спасибо всем, кто до сих пор вносил дополнения (ответы или комментарии), очень важно, что вы написали, чтобы понять поведение компилятора С#.

Из того, что я прочитал, и после рассмотрения возможностей, я выяснил с вашей помощью, что это может быть либо ошибка компилятора, либо поведение по своему замыслу.

Напомним, что у С# были некоторые цели разработки, которые отличают его от языков, подобных C++.

Если вам интересно, что я сделал для дальнейшего исследования: я переименовал внутреннюю функцию в MainL например:

Пример 2б:

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

Этот модифицированный пример компилируется и запускается успешно.

Теперь, когда вы скомпилируете это с помощью LinqPad, а затем переключитесь на вкладку IL, вы увидите, что сделал компилятор:

LabelName

Он создал внутреннюю функцию MainL как g__MainL0_1, MainL функция Main имеет метку g__Main0_0.

Это означает, что если вы удалите L из MainL вы заметите, что компилятор уже переименовывает его уникальным способом, потому что тогда код выглядит так:

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++ рекурсивное разрешение локальных функций может привести к проблемам с рефакторингом, а другие писали, что этот тип поведения в С# - это то, что компилятор делает с локальными переменными (обратите внимание, что сообщение об ошибке совпадает) - все это даже подтверждает, что я думал, что это было сделано по замыслу и не является ошибкой.

Ответы

Ответ 1

Так как Stackoverflow не допускает множественных ответов, я подумал, что было бы наиболее справедливым способом. Я создал этот ответ как вики сообщества, проголосовал за два ответа ниже и добавил их в качестве ссылки для вашей справки:

И я создал резюме в вопросе, содержащее всю информацию, которую я получил от вас из комментариев и ответов:

Ответ 2

Параметры и локальные переменные из охватывающей области доступны внутри локальной функции.

Я думал, что каждый внутренний Main() будет иметь локальную область и скрыт снаружи.

С# не перезаписывает имена из родительской области, поэтому есть и двусмысленность для локального имени 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 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 намного более компактным как в памяти, так и в ЦП.

Ответ 3

Чтобы развернуть бит в ответе 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();
    }
}

Я думал, что каждый внутренний Main() будет иметь локальную область и скрыт снаружи.

Это, но область действия включает имя локальной функции. C.F. что вы не можете переопределить имя переменной из for, foreach или using внутри своей области.

Между тем, я думаю, что это ошибка компилятора.

Это функция компилятора.

Это означает, что удаление L из MainL не должно навредить, потому что компилятор уже переименовывает его уникальным образом, это должно привести к IL-коду, как.

Это означает, что можно ввести ошибку в компилятор, где код, который у вас есть в вашем вопросе, будет работать. Это было бы нарушением правил С# для имен локалей.

он запутан в С#, но логичен в С++

Он блокирует то, что было известно как источник ошибок в течение некоторого времени. Аналогично, в С# вам не разрешено использовать целочисленные значения с if(), и вам нужно явно пропустить в операторах switch. Все это изменения, которые С# сделал с тем, как он сравнивается с С++ в самом начале, и все они удаляют некоторое удобство, но все они - вещи, которые действительно нашли люди, вызванные ошибками и часто запрещенные в соглашениях о кодировании.

Ответ 4

Поскольку С# является статически скомпилированным языком, я думаю, что все функции компилируются до того, как их охватывающая область действия будет выполнена, и поэтому самая внутренняя Main не может быть объявлена, поскольку Main уже существует из ее "точки закрытия" (на один уровень вверх).

Обратите внимание, что это не основано на фактических данных, а только на моих первоначальных мыслях по этому вопросу.

После некоторых исследований... Я был неправ, в основном. Мое чувство кишки, похоже, подразумевает такое же поведение, но не по причинам, которые я изначально думал.

@PetSerAl уже объяснен в форме комментария лучше, чем я мог бы скопировать его из руководства, поэтому я откладываю на этот ответ.