Поделиться через


Выражения верхнего уровня

Заметка

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время дизайна и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия отмечены в соответствующих заседаниях по проектированию языка (LDM).

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Проблема чемпиона: https://github.com/dotnet/csharplang/issues/2765

Сводка

Позвольте последовательности инструкций происходить непосредственно перед namespace_member_declarationв compilation_unit (т. е. исходном файле).

Семантика заключается в том, что если такая последовательность инструкций присутствует, будет выдаваться следующее объявление типа, модулировать фактическое имя метода:

partial class Program
{
    static async Task Main(string[] args)
    {
        // statements
    }
}

См. также https://github.com/dotnet/csharplang/issues/3117.

Мотивация

Существует определенное количество шаблонов, окружающих даже простейшие программы, из-за необходимости явного метода Main. Это, кажется, мешает изучению языков и ясности программ. Основная цель функции заключается в том, чтобы позволить создавать программы на C# без лишнего шаблонного кода, для учащихся, а также для ясности кода.

Подробный дизайн

Синтаксис

Единственным дополнительным синтаксисом является разрешение последовательности инструкций в единице компиляции непосредственно перед namespace_member_declarations:

compilation_unit
    : extern_alias_directive* using_directive* global_attributes? statement* namespace_member_declaration*
    ;

Только одному compilation_unit разрешено содержать инструкции.

Пример:

if (args.Length == 0
    || !int.TryParse(args[0], out int n)
    || n < 0) return;
Console.WriteLine(Fib(n).curr);

(int curr, int prev) Fib(int i)
{
    if (i == 0) return (1, 0);
    var (curr, prev) = Fib(i - 1);
    return (curr + prev, curr);
}

Семантика

Если какие-либо операторы верхнего уровня присутствуют в любой единице компиляции программы, это означает, что они были объединены в блоковый текст метода Main класса Program в глобальном пространстве имен, как показано ниже.

partial class Program
{
    static async Task Main(string[] args)
    {
        // statements
    }
}

Тип называется «Program», поэтому на него можно ссылаться по имени из исходного кода. Это частичный тип, поэтому тип с именем Program в исходном коде также должен быть объявлен как частичный.
Но имя метода Main используется только в целях иллюстрации, фактическое имя, используемое компилятором, зависит от реализации, и метод не может ссылаться по имени из исходного кода.

Метод обозначается как точка входа программы. Явно объявленные методы, которые по соглашению можно рассматривать как кандидаты точек входа, игнорируются. При возникновении этого появляется предупреждение. При наличии операторов верхнего уровня возникает ошибка при указании параметра компилятора -main:<type>.

Метод точки входа всегда имеет один формальный параметр, string[] args. Среда выполнения создает и передает string[] аргумент, содержащий аргументы командной строки, указанные при запуске приложения. Аргумент string[] никогда не имеет значения NULL, но может иметь длину нуля, если аргументы командной строки не были указаны. Параметр args находится в области видимости внутри операторов верхнего уровня, но находится вне области их действия. Применяются обычные правила конфликтов имен и тени.

Асинхронные операции допускаются в выражениях верхнего уровня в той степени, в которой они разрешены в рамках инструкций обычного асинхронного метода точки входа. Однако они не обязательны, если await выражения и другие асинхронные операции исключены, предупреждение не выводится.

Сигнатура созданного метода точки входа определяется исходя из операций, используемых инструкциями верхнего уровня следующим образом:

Async-operations\Return-with-expression настоящее Отсутствует
настоящее static Task<int> Main(string[] args) static Task Main(string[] args)
Отсутствует static int Main(string[] args) static void Main(string[] args)

В приведенном выше примере будет приведено следующее объявление метода $Main:

partial class Program
{
    static void $Main(string[] args)
    {
        if (args.Length == 0
            || !int.TryParse(args[0], out int n)
            || n < 0) return;
        Console.WriteLine(Fib(n).curr);
        
        (int curr, int prev) Fib(int i)
        {
            if (i == 0) return (1, 0);
            var (curr, prev) = Fib(i - 1);
            return (curr + prev, curr);
        }
    }
}

В то же время такой пример:

await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");

будет давать:

partial class Program
{
    static async Task $Main(string[] args)
    {
        await System.Threading.Tasks.Task.Delay(1000);
        System.Console.WriteLine("Hi!");
    }
}

Пример, как показано ниже:

await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");
return 0;

будет давать:

partial class Program
{
    static async Task<int> $Main(string[] args)
    {
        await System.Threading.Tasks.Task.Delay(1000);
        System.Console.WriteLine("Hi!");
        return 0;
    }
}

И пример, как показано ниже:

System.Console.WriteLine("Hi!");
return 2;

могла бы дать:

partial class Program
{
    static int $Main(string[] args)
    {
        System.Console.WriteLine("Hi!");
        return 2;
    }
}

Область локальных переменных верхнего уровня и локальных функций

Несмотря на то, что локальные переменные и функции верхнего уровня "оборачиваются" в созданный метод точки входа, они по-прежнему должны находиться в области видимости программы в каждой единице компиляции. Для оценки простого имени по достижении глобального пространства имен:

  • Сначала предпринята попытка оценить имя в созданном методе точки входа и только в случае сбоя этой попытки
  • Выполняется обычная оценка в объявлении глобального пространства имен.

Это может привести к затенению пространств имен и типов, объявленных в глобальном пространстве имен, а также к затенению импортированных имен.

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

Таким образом, мы защищаем нашу будущую способность лучше решать "функции верхнего уровня" (сценарий 2 в https://github.com/dotnet/csharplang/issues/3117) и иметь возможность предоставлять полезную диагностику пользователям, которые ошибочно считают, что они поддерживаются.