深入理解 C# 7.1 提供的 async 非同步 Main() 方法
我們在開發 .NET 應用程式的時候,預設選取的 C# 語言版本為「最新已發行主要版本」(latest major version),如果以 Visual Studio 2017 v15.9.10 來說,內建的 C# 最新發行版本就是 7.3
版,因此主要版本就是 7.0
版。本篇文章要來介紹 C# 7.1 提供的一個語法糖,它能讓你的 Console 應用程式,將主程式的進入點 Main()
方法也能宣告為非同步(async)的版本,好讓你從頭到尾都用非同步的方式開發應用程式,最後還會說明這個新語法背後的技術原理。
預設的 Console 應用程式
通常剛建立好一個 Console 應用程式專案,其主程式 Program.cs
的內容如下:
1 using System; 2 using System.Threading.Tasks; 3 4 namespace ConsoleApp 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 // Your code here 11 } 12 } 13 }
由於 Visual Studio 2017 預設主控台應用程式的專案範本,預設僅支援到 C# 7.0 版,也就是說,你沒辦法寫出以下的程式碼,專案建置的時候會遇到 error CS5001: 程式未包含適合進入點的靜態 'Main' 方法
與 CS8107: C# 7.0 中未提供功能 '非同步主要'。請使用語言版本 7.1 或更高版本。
的錯誤訊息:
1 using System; 2 using System.Threading.Tasks; 3 4 namespace ConsoleApp 5 { 6 class Program 7 { 8 static async Task Main(string[] args) 9 { 10 // Your code here 11 } 12 } 13 }
使用 async/await 開發 Console 應用程式
解決這個問題還挺簡單,基本上有兩種方法:
-
呼叫
Task.Run(async () => { ... }).Wait();
即可以下用一段簡單的 HttpClient 範例示範寫法:
1 using System; 2 using System.Net.Http; 3 using System.Threading.Tasks; 4 5 namespace ConsoleApp 6 { 7 class Program 8 { 9 static void Main(string[] args) 10 { 11 Task.Run(async () => 12 { 13 using (var http = new HttpClient()) 14 { 15 const string url = "http://docs.microsoft.com/"; 16 var body = await http.GetStringAsync(url); 17 Console.WriteLine($"Size: {body.Length}"); 18 } 19 }).Wait(); 20 } 21 } 22 }
2. 切換到 C# 7.1 以上的語言版本
無論你用 .NET Framework 或 .NET Core 的 Console App,都可以直接手動修改 *.csproj
專案檔,只要在第一個 <PropertyGroup>
底下加入 <LangVersion>latest</LangVersion>
即可:
1 <LangVersion>latest</LangVersion>
強烈建議手動編輯 *.csproj
加入 <LangVersion>
設定。
成功加入之後,就可以直接以 async Task
來宣告 Main()
方法,以下用一段簡單的 HttpClient 範例示範寫法:
1 using System; 2 using System.Net.Http; 3 using System.Threading.Tasks; 4 5 namespace ConsoleApp 6 { 7 class Program 8 { 9 static async Task Main(string[] args) 10 { 11 using (var http = new HttpClient()) 12 { 13 const string url = "http://docs.microsoft.com/"; 14 var body = await http.GetStringAsync(url); 15 Console.WriteLine($"Size: {body.Length}"); 16 } 17 } 18 } 19 }
你也可以透過 msbuild /p:LangVersion=latest
命令,自動覆寫 csproj
檔案中的預設值。
深入 static async Task Main(string[] args)
的技術細節
光是調整設定好像有點無趣,我們來看看套用 C# 7.1 的 async Main 之後,C# 編譯器在背後偷偷做了什麼事!
我先將上一段程式碼進行編譯,產生 ConsoleApp.exe
執行檔,並且用 ILSpy 進行反組譯分析。由於 C# 7.1 在編譯這段程式碼時,會自動對程式碼進行調整,之前有提過 C# 7.1 的 async Main 其實是個語法糖,也就是說程式碼在編譯完成後,將會跟原本的不太一樣。
在進行分析之前,請先進行以下設定,開啟 Show all types and members 選項:
我們要進行反組譯分析的第一步,就是找到程式的進入點。此時你會發現,原來 C# 7.1 並沒有真的將 static async Task Main(string[] args)
當成程式的進入點,而是另外建立一個 傳統的 Main()
方法,以這個 private static void <Main>(string[] args)
當做程式的進入點:
1 private static void <Main>(string[] args) 2 { 3 Main(args).GetAwaiter().GetResult(); 4 }
這裡的 <Main>
方法是由編譯器自動產生的方法,他呼叫了一個 Main()
方法,這個正是你在 Visual Studio 中撰寫的主程式,其回傳型別是 Task
。然後直接呼叫了 Task.GetAwaiter Method 取得 TaskAwaiter,然後再呼叫 TaskAwaiter.GetResult Method 結束對非同步工作完成的等候。
請注意:GetAwaiter()
與 GetResult()
都不建議自己寫在應用程式中,這兩個方法僅供編譯器使用。
原始碼本身可以解釋一切,如果有興趣的話,可以進一步查看 <Main>d__0
這個類別的內容,這是一份 IAsyncStateMachine 的實作,用來控制 async/await 的執行流程,必要的時候還要翻開 IL 才能看懂整個執行過程。我相信透過這個反組譯的過程,可以幫助開發人員更加理解 async/await 的技術細節與設計原理,也能更能有自信的撰寫非同步的程式碼。