Почему локальная функция не всегда скрыта в С# 7?
То, что я показываю ниже, это скорее теоретический вопрос. Но меня интересует, как новый компилятор С# 7 работает и разрешает локальные функции.
В С# 7 я могу использовать локальные функции. Например (вы можете попробовать эти примеры в бета-версии LinqPad):
Пример 1: Вложенный Main()
void Main()
{
void Main()
{
Console.WriteLine("Hello!");
}
Main();
}
DotNetFiddle для примера 1
Вместо того, чтобы вызывать Main()
рекурсивным способом, локальная функция Main()
вызывается один раз, поэтому результат этого:
Здравствуйте!
Компилятор принимает это без предупреждений и ошибок.
Пример 2: Здесь я иду на один уровень глубже, например:
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 уже объяснен в форме комментария лучше, чем я мог бы скопировать его из руководства, поэтому я откладываю на этот ответ.