C#6.0 新增功能

  • 2019 年 10 月 4 日
  • 筆記

C# 6.0 版本包含許多可提高開發人員工作效率的功能。 這些功能的總體效果是讓你編寫的程式碼更簡潔、更具可讀性。 該語法不像許多常見做法那樣繁瑣。 可以更輕鬆地看出設計意圖。 好好了解這些功能可以幫助你提高生產力,編寫更具可讀性的程式碼。 你可以更專註於功能,而不是語言的構造。

本文的其餘部分是對每個功能的概述,並提供用於探索每個功能的鏈接。 還可以在教程部分的 C# 6 互動式探索中探索這些功能。

01 只讀自動屬性

只讀自動屬性提供了更簡潔的語法來創建不可變類型。 你聲明僅具有 get 訪問器的自動屬性:

public string FirstName { get; }  public string LastName { get;  }

FirstNameLastName 屬性只能在構造函數的主體中設置;

嘗試在另一種普通方法中設置 LastName 會生成 CS0200 編譯錯誤:

此功能實現用於創建不可變類型的真正語言支援且使用更簡潔和方便的自動屬性語法。

02 自動屬性初始化表達式

自動屬性初始值設定項可讓你在屬性聲明中聲明自動屬性的初始值。

   public class Student      {          public string FirstName { get; } = "張";          public string LastName { get; private set; } = "傳寧";      }

FirstName,LaseName 成員在聲明它的位置處被初始化。 這樣,就能更容易地僅執行一次初始化。 初始化是屬性聲明的一部分,可更輕鬆地將存儲分配。

03 Expression-bodied(正文表達式) 函數成員

你編寫的許多成員是可以作為單個表達式的單個語句。 改為編寫 expression-bodied 成員。這適用於方法和只讀屬性。 例如,重寫 ToString() 通常是理想之選:

public override string ToString() => $"{LastName}, {FirstName}";

也可以將此語法用於只讀屬性:

public string FullName => $"{FirstName} {LastName}";

將現有成員更改為 expression bodied 成員是二進位兼容的更改

04 靜態導入 using static

using static 增強功能可用於導入單個類的靜態方法。 指定要使用的類:

using static System.Math;

Math 不包含任何實例方法。 還可以使用 using static 為具有靜態和實例方法的類導入類的靜態方法。 最有用的示例之一是 String

using static System.String;

在 using static 語句中必須使用完全限定的類名 System.String。 而不能使用 string 關鍵字。

static using 語句導入時,僅在使用擴展方法調用語法調用擴展方法時,擴展方法才在範圍內。 作為靜態方法調用時,擴展方法不在範圍內。 你在 LINQ 查詢中會經常看到這種情況。 可以通過導入 EnumerableQueryable 來導入 LINQ 模式。

using static System.Linq.Enumerable;

通常使用擴展方法調用表達式調用擴展方法。 在使用靜態方法調用語法對其進行調用的罕見情況下,添加類名稱可以解決歧義。

static using 指令還可以導入任何嵌套的類型。 可以引用任何嵌套的類型,而無需限定。

看下面的一個具體事例:

舊語法:

using System;    namespace Demo002_NF46_CS60  {      class Program      {          static void Main(string[] args)          {              Console.WriteLine("Hello world");          }      }  }

引入 using static 語法:

using static System.Console;    namespace Demo002_NF46_CS60  {      class Program      {          static void Main(string[] args)          {             WriteLine("Hello world");          }      }  }

關於using static 的更具體的資訊,請參考《using 靜態指令》

05 Null 條件運算符

Null 條件運算符使 null 檢查更輕鬆、更流暢。 將成員訪問 . 替換為 ?.

var first = person?.FirstName;

在前面的示例中,如果 Person 對象是 null,則將變數 first 賦值為 null。 否則,將 FirstName 屬性的值分配給該變數。 最重要的是?. 意味著當 person 變數為 null 時,此行程式碼不會生成 NullReferenceException。 它會短路並返回 null。 還可以將 null 條件運算符用於數組或索引器訪問。 將索引表達式中的 [] 替換為 ?[]

當 FirstName 為 null 時,變數 firstName 為 null,列印輸出時不報錯:

無論 person 的值是什麼,以下表達式均返回 string。 通常,將此構造與「null 合併」運算符一起使用,以在其中一個屬性為 null 時分配默認值。 表達式短路時,鍵入返回的 null值以匹配整個表達式。

first = person?.FirstName ?? "Unspecified";

還可以將 ?. 用於有條件地調用方法。 具有 null 條件運算符的成員函數的最常見用法是用於安全地調用可能為 null 的委託(或事件處理程式)。 通過使用 ?. 運算符調用該委託的 Invoke 方法來訪問成員。 可以在委託模式一文中看到示例。

?. 運算符的規則確保運算符的左側僅計算一次。 它支援許多語法,包括使用事件處理程式的以下示例:

// preferred in C# 6:  this.SomethingHappened?.Invoke(this, eventArgs);

確保左側只計算一次,這使得你可以在 ?. 的左側使用任何表達式(包括方法調用)。

06 字元串內插

使用 C# 6,新的字元串內插功能可以在字元串中嵌入表達式。 使用 $ 作為字元串的開頭,並使用 {} 之間的表達式代替序號:

public string FullName => $"{FirstName} {LastName}";

本示例使用替代表達式的屬性。 可以使用任何表達式。 例如,可以在內插過程中計算學生的成績平均值:

public string GetGradePointPercentage() => $"Name: {LastName}, {FirstName}. G.P.A: {Grades.Average():F2}";

上一行程式碼將 Grades.Average() 的值格式設置為具有兩位小數的浮點數。

通常,可能需要使用特定區域性設置生成的字元串的格式。 請利用通過字元串內插生成的對象可以隱式轉換為 System.FormattableString 這一事實。 FormattableString 實例包含組合格式字元串,以及在將其轉換為字元串之前評估表達式的結果。 在設置字元串的格式時,可以使用 FormattableString.ToString(IFormatProvider) 方法指定區域性。 下面的示例使用德語 (de-DE) 區域性生成字元串。 (德語區域性默認使用「,」字元作為小數分隔符,使用「.」字元作為千位分隔符。)

FormattableString str = $"Average grade is {s.Grades.Average()}";  var gradeStr = str.ToString(new System.Globalization.CultureInfo("de-DE"));

要開始使用字元串內插,請參閱 字元串內插 一文和 C# 中字元串內插符合格式設置 教程。

07 異常篩選器

「異常篩選器」是確定何時應該應用給定的 catch 子句的子句。 如果用於異常篩選器的表達式計算結果為 true,則 catch 子句將對異常執行正常處理。 如果表達式計算結果為 false,則將跳過 catch 子句。 一種用途是檢查有關異常的資訊,以確定 catch 子句是否可以處理該異常:

public static async Task<string> MakeRequest()  {      WebRequestHandler webRequestHandler = new WebRequestHandler();      webRequestHandler.AllowAutoRedirect = false;      using (HttpClient client = new HttpClient(webRequestHandler))      {          var stringTask = client.GetStringAsync("https://docs.microsoft.com/en-us/dotnet/about/");          try          {              var responseText = await stringTask;              return responseText;          }          catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))          {              return "Site Moved";          }      }  }

相當於

 catch (System.Net.Http.HttpRequestException e)   {      if(e.Message.Contains("301")) // 如果判斷的邏輯較多,建議使用該方式。      {          return "Site Moved";      }   }

08 nameof 表達式

nameof 表達式的計算結果為符號的名稱。 每當需要變數、屬性或成員欄位的名稱時,這是讓工具正常運行的好辦法。 nameof 的其中一個最常見的用途是提供引起異常的符號的名稱:

if (IsNullOrWhiteSpace(lastName))  {     throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));  }

另一個用途是用於實現 INotifyPropertyChanged 介面的基於 XAML 的應用程式:

private string lastName;  public string LastName  {      get { return lastName; }      set      {          if (value != lastName)          {              lastName = value;              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LastName)));          }      }  }

09 Catch 和 Finally 塊中的 Await

C# 5 對於可放置 await 表達式的位置有若干限制。 使用 C# 6,現在可以在 catchfinally 表達式中使用 await。 這通常用於日誌記錄方案:

public static async Task<string> MakeRequestAndLogFailures()  {      await logMethodEntrance();      var client = new System.Net.Http.HttpClient();      var streamTask = client.GetStringAsync("https://localHost:10000");      try      {          var responseText = await streamTask;          return responseText;      } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))      {          await logError("Recovered from redirect", e);          return "Site Moved";      }      finally      {          await logMethodExit();          client.Dispose();      }  }

catchfinally 子句中添加 await 支援的實現細節可確保該行為與同步程式碼的行為一致。 當在 catchfinally 子句中執行的程式碼引發異常時,執行將在下一個外層塊中查找合適的 catch 子句。 如果存在當前異常,則該異常將丟失。 catchfinally 子句中的 awaited 表達式也會發生同樣的情況:搜索合適的 catch,並且當前異常(如果有)將丟失。

鑒於此行為,建議仔細編寫 catch 和 finally 子句,避免引入新的異常。

10 使用索引器初始化關聯集合

索引初始值設定項是提高集合初始值設定項與索引用途一致性的兩個功能之一。 在早期版本的 C# 中,可以將集合初始值設定項用於序列樣式集合,包括在鍵值對周圍添加括弧而得到 Dictionary<TKey,TValue>

private Dictionary<int, string> messages = new Dictionary<int, string>  {      { 404, "Page not Found"},      { 302, "Page moved, but left a forwarding address."},      { 500, "The web server can't come out to play today."}  };

可以將集合初始值設定項與 Dictionary<TKey,TValue> 集合和其他類型一起使用,在這種情況下,可訪問的 Add 方法接受多個參數。 新語法支援使用索引分配到集合中:

private Dictionary<int, string> webErrors = new Dictionary<int, string>  {      [404] = "Page not Found",      [302] = "Page moved, but left a forwarding address.",      [500] = "The web server can't come out to play today."  };

此功能意味著,可以使用與多個版本中已有的序列容器語法類似的語法初始化關聯容器。

11 集合初始值設定項中的擴展 Add 方法

使集合初始化更容易的另一個功能是對 Add 方法使用擴展方法。 添加此功能的目的是進行 Visual Basic 的奇偶校驗。 如果自定義集合類的方法具有通過語義方式添加新項的名稱,則此功能非常有用。

12 改進了重載解析

在以前的一些構造中,以前版本的 C# 編譯器可能會發現涉及 lambda 表達式的一些方法不明確。 請考慮此方法:

static Task DoThings()  {       return Task.FromResult(0);  }

在早期版本的 C# 中,使用方法組語法調用該方法將失敗:

Task.Run(DoThings);

早期的編譯器無法正確區分 Task.Run(Action)Task.Run(Func<Task>())。 在早期版本中,需要使用 lambda 表達式作為參數:

Task.Run(() => DoThings());

C# 6 編譯器正確地確定 Task.Run(Func<Task>()) 是更好的選擇。

確定性的編譯器選項

-deterministic 選項指示編譯器為同一源文件的後續編譯生成完全相同的輸出程式集。

默認情況下,每個編譯都生成唯一的輸出內容。 編譯器添加一個時間戳和一個隨機生成的 GUID。 如果想按位元組比較輸出以確保各項生成之間的一致性,請使用此選項。