﻿# 深入理解 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` 的內容如下：

```csharp
using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      // Your code here
    }
  }
}
```

由於 Visual Studio 2017 預設主控台應用程式的專案範本，預設僅支援到 C# 7.0 版，也就是說，你沒辦法寫出以下的程式碼，專案建置的時候會遇到 `error CS5001: 程式未包含適合進入點的靜態 'Main' 方法` 與 `CS8107: C# 7.0 中未提供功能 '非同步主要'。請使用語言版本 7.1 或更高版本。` 的錯誤訊息：

```csharp
using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
  class Program
  {
    static async Task Main(string[] args)
    {
      // Your code here
    }
  }
}
```

### 使用 async/await 開發 Console 應用程式

解決這個問題還挺簡單，基本上有兩種方法：

1.  呼叫 `Task.Run(async () => { ... }).Wait();` 即可
    
    以下用一段簡單的 HttpClient 範例示範寫法：
    
    ```csharp
    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    namespace ConsoleApp
    {
      class Program
      {
        static void Main(string[] args)
        {
          Task.Run(async () =>
          {
            using (var http = new HttpClient())
            {
              const string url = "http://docs.microsoft.com/";
              var body = await http.GetStringAsync(url);
              Console.WriteLine($"Size: {body.Length}");
            }
          }).Wait();
        }
      }
    }
    ```
    
2.  切換到 C# 7.1 以上的語言版本
    
    無論你用 .NET Framework 或 .NET Core 的 Console App，都可以直接手動修改 `*.csproj` 專案檔，只要在第一個 `<PropertyGroup>` 底下加入 `<LangVersion>latest</LangVersion>` 即可：
    
    ```xml
    <LangVersion>latest</LangVersion>
    ```
    
    > 強烈建議手動編輯 `*.csproj` 加入 `<LangVersion>` 設定。
    
    成功加入之後，就可以直接以 `async Task` 來宣告 `Main()` 方法，以下用一段簡單的 HttpClient 範例示範寫法：
    
    ```csharp
    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    namespace ConsoleApp
    {
      class Program
      {
        static async Task Main(string[] args)
        {
          using (var http = new HttpClient())
          {
            const string url = "http://docs.microsoft.com/";
            var body = await http.GetStringAsync(url);
            Console.WriteLine($"Size: {body.Length}");
          }
        }
      }
    }
    ```
    
    > 你也可以透過 `msbuild /p:LangVersion=latest` 命令，自動覆寫 `csproj` 檔案中的預設值。
    

### 深入 `static async Task Main(string[] args)` 的技術細節

光是調整設定好像有點無趣，我們來看看套用 C# 7.1 的 async Main 之後，C# 編譯器在背後偷偷做了什麼事！

我先將上一段程式碼進行編譯，產生 `ConsoleApp.exe` 執行檔，並且用 [ILSpy](https://github.com/icsharpcode/ILSpy) 進行反組譯分析。由於 C# 7.1 在編譯這段程式碼時，會自動對程式碼進行調整，之前有提過 C# 7.1 的 async Main 其實是個**語法糖**，也就是說程式碼在編譯完成後，將會跟原本的不太一樣。

在進行分析之前，請先進行以下設定，開啟 **Show all types and members** 選項：

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/51d89f6f4996e82ec61c-image_3194.png)

我們要進行反組譯分析的第一步，就是找到程式的進入點。此時你會發現，原來 C# 7.1 並沒有真的將 `static async Task Main(string[] args)` 當成程式的進入點，而是另外建立一個 **傳統的** `Main()` 方法，以這個 `private static void <Main>(string[] args)` 當做程式的進入點：

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/de8cb4b07a0e0ea9eadf-image_3195.png)

```csharp
private static void <Main>(string[] args)
{
    Main(args).GetAwaiter().GetResult();
}
```

這裡的 `<Main>` 方法是由編譯器自動產生的方法，他呼叫了一個 `Main()` 方法，這個正是你在 Visual Studio 中撰寫的主程式，其回傳型別是 `Task`。然後直接呼叫了 [Task.GetAwaiter Method](https://learn.microsoft.com/zh-tw/dotnet/api/system.threading.tasks.task.getawaiter?view=netframework-4.7.2) 取得 [TaskAwaiter](https://learn.microsoft.com/zh-tw/dotnet/api/system.runtime.compilerservices.taskawaiter?view=netframework-4.7.2)，然後再呼叫 [TaskAwaiter.GetResult Method](https://learn.microsoft.com/zh-tw/dotnet/api/system.runtime.compilerservices.taskawaiter.getresult?view=netframework-4.7.2#System_Runtime_CompilerServices_TaskAwaiter_GetResult) 結束對非同步工作完成的等候。

> 請注意：`GetAwaiter()` 與 `GetResult()` 都不建議自己寫在應用程式中，這兩個方法僅供編譯器使用。

原始碼本身可以解釋一切，如果有興趣的話，可以進一步查看 `<Main>d__0` 這個類別的內容，這是一份 [IAsyncStateMachine](https://learn.microsoft.com/zh-tw/dotnet/api/system.runtime.compilerservices.iasyncstatemachine?view=netframework-4.7.2) 的實作，用來控制 async/await 的執行流程，必要的時候還要翻開 IL 才能看懂整個執行過程。我相信透過這個反組譯的過程，可以幫助開發人員更加理解 async/await 的技術細節與設計原理，也能更能有自信的撰寫非同步的程式碼。

### 相關連結

-   [選取 C# 語言版本 - C# 指南 | Microsoft Docs](https://learn.microsoft.com/zh-tw/dotnet/csharp/language-reference/configure-language-version)
-   [async at console app in C#? - Stack Overflow](https://stackoverflow.com/questions/17630506/async-at-console-app-in-c)
-   [Using C# 7.1 with MSBuild - Stack Overflow](https://stackoverflow.com/questions/46419760/using-c-sharp-7-1-with-msbuild)
-   [C# 7.1 的 Async Main() | 微軟開發工具資訊分享 - 點部落](https://dotblogs.com.tw/aspnetshare/2017/08/10/asyncmain)
-   [SharpLab](https://sharplab.io/) (線上版的 .NET 反組譯工具)
-   [ILSpy](https://github.com/icsharpcode/ILSpy)
