頂層陳述
註釋
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的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);
}
語義學
如果程式的任何編譯單位中有任何最上層語句,意義就如同它們結合在全域命名空間中 Program
類別 Main
方法的區塊主體中,如下所示:
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;
}
}
最上層局部變數和局部函數的範圍
即使最上層局部變數和函式會被嵌入到產生的進入點方法中,它們仍應該在每個編譯單位的整個程式範圍內。 為了進行簡單名稱的評估,一旦到達全域命名空間:
- 首先,我們會嘗試在生成的進入點方法中評估名稱,只有在嘗試失敗時才會進行其他操作。
- 在全域命名空間宣告中執行「常規」評估。
這可能會導致在全域命名空間內宣告的命名空間和型別名稱遮蔽,以及匯入名稱的遮蔽。
如果簡單名稱評估發生在最上層語句之外,而且評估會產生最上層局部變數或函式,則這應該會導致錯誤。
如此一來,我們就能保護未來更好地處理「最上層函式」的能力(https://github.com/dotnet/csharplang/issues/3117中的案例 2),並且能夠對誤認為支持這些功能的使用者提供有用的診斷。