顶级语句

注意

本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。

可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。

支持者问题:https://github.com/dotnet/csharplang/issues/2765

总结

允许语句序列出现在 compilation_unit(即源文件)的 namespace_member_declaration 之前。

其语义是,如果存在这样的语句序列,就会发出以下类型声明,并对实际方法名称进行修改:

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

另请参阅 https://github.com/dotnet/csharplang/issues/3117

动力

即使是最简单的程序,也要包含一定量的模板代码,因为需要显式的 Main 方法。 这似乎妨碍了语言学习和程序清晰。 因此,该功能的主要目标是使 C# 程序没有不必要的样板代码,以便于学习者和代码的清晰。

详细设计

语法

唯一的附加语法是允许在一个编译单元中,在 namespace_member_declaration 之前使用语句序列:

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

语义学

如果程序的任何编译单元中存在任何顶级语句,则含义就像它们合并到全局命名空间中 ProgramMain 方法的块体中一样,如下所示:

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

该类型名为“Program”,因此可以通过源代码中的名称引用。 它是部分类型,因此源代码中名为“Program”的类型也必须声明为分部类型。
但方法名称“Main”仅用于说明目的,编译器使用的实际名称依赖于实现,并且该方法不能由源代码中的名称引用。

该方法被指定为程序的入口点。 按照惯例可被视为入口候选点的显式声明的方法将被忽略。 出现这种情况时,系统会发出警告。 当有顶层语句时,指定 -main:<type> 编译器开关是错误的。

入口点方法始终具有一个正式参数,string[] args。 执行环境创建并传递一个包含启动应用程序时指定的命令行参数的 string[] 参数。 string[] 参数从不为 null,但如果未指定命令行参数,则其长度可能为零。 “args”参数位于顶级语句的作用域内,不在它们之外的范围内。 常规名称冲突/遮蔽规则适用。

在顶层语句中允许进行异步操作的程度与在常规异步入口点方法中允许进行异步操作的程度相同。 但是,如果省略 await 表达式和其他异步操作,则不需要它们,则不会生成任何警告。

生成的入口点方法的签名是根据顶层语句使用的操作确定的,如下所示:

异步操作\带表达式的返回 显示 缺席
显示 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;
    }
}

顶级局部变量和局部函数的范围

尽管顶级局部变量和函数被“包装”到生成的入口点方法中,但它们仍应该在每个编译单元的整个程序中处于范围内。 就简单名称评估而言,一旦到达全局命名空间,就会对该名称进行评估:

  • 首先,我们会尝试在生成的入口点方法中对名称进行评估,并且只会在评估失败时进行
  • 在全局命名空间声明中执行“常规”评估。

这可能会导致在全局名称空间中声明的命名空间和类型的名称以及导入的名称被覆盖。

如果简单名称求值发生在顶级语句之外,并且计算生成顶级局部变量或函数,则会导致错误。

这样,我们就可以保护我们未来的能力,以更好地解决“顶级函数”(https://github.com/dotnet/csharplang/issues/3117中的方案 2),并能够向错误地相信支持它们的用户提供有用的诊断。