C#8.0 新增功能

  • 2019 年 10 月 4 日
  • 筆記

C#8.0提供了許多增強功能

01 Readonly 成員

可將 readonly 修飾符應用於結構的任何成員。 它指示該成員不會修改狀態。 這比將 readonly 修飾符應用於 struct 聲明更精細。 請考慮以下可變結構:

public struct Point  {      public double X { get; set; }      public double Y { get; set; }      public double Distance => Math.Sqrt(X * X + Y * Y);        public override string ToString() => $"({X}, {Y}) is {Distance} from the origin";  }

像大多數結構一樣, ToString() 方法不會修改狀態。 可以通過將 readonly 修飾符添加到 ToString() 的聲明來對此進行指示:

public readonly override string ToString() => $"({X}, {Y}) is {Distance} from the origin";

上述更改會生成編譯器警告,因為 ToString 訪問 Distance 屬性,該屬性未標記為 readonly

warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an implicit copy of 'this'

需要創建防禦性副本時,編譯器會發出警告。 Distance 屬性不會更改狀態,因此可以通過將 readonly 修飾符添加到聲明來修復此警告:

public readonly double Distance => Math.Sqrt(X * X + Y * Y);

請注意,readonly 修飾符對於只讀屬性是必需的。 編譯器不會假設 get 訪問器不修改狀態;必須明確聲明 readonly。 編譯器會強制實施以下規則:readonly 成員不修改狀態。除非刪除 readonly 修飾符,否則不會編譯以下方法:

public readonly void Translate(int xOffset, int yOffset)  {      X += xOffset;      Y += yOffset;  }

通過此功能,可以指定設計意圖,使編譯器可以強制執行該意圖,並基於該意圖進行優化。

02 默認介面成員【*重要*】

現在可以將成員添加到介面,並為這些成員提供實現。 藉助此語言功能,API 作者可以將方法添加到以後版本的介面中,而不會破壞與該介面當前實現的源或二進位文件兼容性。 現有的實現繼承默認實現 。 此功能使 C# 與面向 Android 或 Swift 的 API 進行互操作,此類 API 支援類似功能。 默認介面成員還支援類似於「特徵」語言功能的方案。

默認介面成員會影響很多方案和語言元素。 請參考 C#8.0 中使用默認介面成員更新介面

03 在更多位置中使用更多模式

模式匹配 提供了在相關但不同類型的數據中提供形狀相關功能的工具。 C# 7.0 通過使用 is表達式和 switch 語句引入了類型模式和常量模式的語法。 這些功能代表了支援數據和功能分離的編程範例的初步嘗試。 隨著行業轉向更多微服務和其他基於雲的體系結構,還需要其他語言工具。

C# 8.0 擴展了此辭彙表,這樣就可以在程式碼中的更多位置使用更多模式表達式。 當數據和功能分離時,請考慮使用這些功能。 當演算法依賴於對象運行時類型以外的事實時,請考慮使用模式匹配。 這些技術提供了另一種表達設計的方式。

除了可以在新位置使用新模式之外,C# 8.0 還添加了「遞歸模式」 。 任何模式表達式的結果都是一個表達式。 遞歸模式只是應用於另一個模式表達式輸出的模式表達式。

Switch 表達式

通常情況下,switch 語句在其每個 case 塊中生成一個值。 藉助 Switch 表達式 ,可以使用更簡潔的表達式語法。 只有些許重複的 casebreak 關鍵字和大括弧。 以下面列出彩虹顏色的枚舉為例:

public enum Rainbow  {      Red,      Orange,      Yellow,      Green,      Blue,      Indigo,      Violet  }

如果應用定義了通過 RGB 組件構造而成的 RGBColor 類型,可使用以下包含 switch 表達式的方法,將 Rainbow 轉換為 RGB 值:

public static RGBColor FromRainbow(Rainbow colorBand) =>      colorBand switch      {          Rainbow.Red    => new RGBColor(0xFF, 0x00, 0x00),          Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),          Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),          Rainbow.Green  => new RGBColor(0x00, 0xFF, 0x00),          Rainbow.Blue   => new RGBColor(0x00, 0x00, 0xFF),          Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),          Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),          _              => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),      };

這裡有幾個語法改進:

  • 變數位於 switch 關鍵字之前。 不同的順序使得在視覺上可以很輕鬆地區分 switch 表達式和 switch 語句。
  • case: 元素替換為 =>。 它更簡潔,更直觀。
  • default 事例替換為 _ 棄元。
  • 正文是表達式,不是語句。

將其與使用經典 switch 語句的等效程式碼進行對比:

public static RGBColor FromRainbowClassic(Rainbow colorBand)  {      switch (colorBand)      {          case Rainbow.Red:              return new RGBColor(0xFF, 0x00, 0x00);          case Rainbow.Orange:              return new RGBColor(0xFF, 0x7F, 0x00);          case Rainbow.Yellow:              return new RGBColor(0xFF, 0xFF, 0x00);          case Rainbow.Green:              return new RGBColor(0x00, 0xFF, 0x00);          case Rainbow.Blue:              return new RGBColor(0x00, 0x00, 0xFF);          case Rainbow.Indigo:              return new RGBColor(0x4B, 0x00, 0x82);          case Rainbow.Violet:              return new RGBColor(0x94, 0x00, 0xD3);          default:              throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand));      };  }

屬性模式

藉助屬性模式 ,可以匹配所檢查的對象的屬性。 請看一個電子商務網站的示例,該網站必須根據買家地址計算銷售稅。 這種計算不是 Address 類的核心職責。 它會隨時間變化,可能比地址格式的更改更頻繁。 銷售稅的金額取決於地址的 State 屬性。 下面的方法使用屬性模式從地址和價格計算銷售稅:

public static decimal ComputeSalesTax(Address location, decimal salePrice) =>      location switch      {          { State: "WA" } => salePrice * 0.06M,          { State: "MN" } => salePrice * 0.75M,          { State: "MI" } => salePrice * 0.05M,          // other cases removed for brevity...          _ => 0M      };

模式匹配為表達此演算法創建了簡潔的語法。

元組模式

一些演算法依賴於多個輸入。 使用元組模式,可根據表示為元組的多個值進行切換 。 以下程式碼顯示了遊戲「rock, paper, scissors(石頭剪刀布)」的切換表達式: :

public static string RockPaperScissors(string first, string second)      => (first, second) switch      {          ("rock", "paper") => "rock is covered by paper. Paper wins.",          ("rock", "scissors") => "rock breaks scissors. Rock wins.",          ("paper", "rock") => "paper covers rock. Paper wins.",          ("paper", "scissors") => "paper is cut by scissors. Scissors wins.",          ("scissors", "rock") => "scissors is broken by rock. Rock wins.",          ("scissors", "paper") => "scissors cuts paper. Scissors wins.",          (_, _) => "tie"      };

消息指示獲勝者。 棄元表示平局(石頭剪刀布遊戲)的三種組合或其他文本輸入。

位置模式

某些類型包含 Deconstruct 方法,該方法將其屬性解構為離散變數。 如果可以訪問 Deconstruct 方法,就可以使用位置模式 檢查對象的屬性並將這些屬性用於模式。 考慮以下 Point 類,其中包含用於為 XY 創建離散變數的 Deconstruct 方法:

public class Point  {      public int X { get; }      public int Y { get; }        public Point(int x, int y) => (X, Y) = (x, y);        public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);  }

此外,請考慮以下表示象限的各種位置的枚舉:

public enum Quadrant  {      Unknown,      Origin,      One,      Two,      Three,      Four,      OnBorder  }

下面的方法使用位置模式 來提取 xy 的值。 然後,它使用 when 子句來確定該點的 Quadrant

static Quadrant GetQuadrant(Point point) => point switch  {      (0, 0) => Quadrant.Origin,      var (x, y) when x > 0 && y > 0 => Quadrant.One,      var (x, y) when x < 0 && y > 0 => Quadrant.Two,      var (x, y) when x < 0 && y < 0 => Quadrant.Three,      var (x, y) when x > 0 && y < 0 => Quadrant.Four,      var (_, _) => Quadrant.OnBorder,      _ => Quadrant.Unknown  };

xy 為 0(但不是兩者同時為 0)時,前一個開關中的棄元模式匹配。 Switch 表達式必須要麼生成值,要麼引發異常。 如果這些情況都不匹配,則 switch 表達式將引發異常。如果沒有在 switch 表達式中涵蓋所有可能的情況,編譯器將生成一個警告。

可在此模式匹配高級教程中探索模式匹配方法。

04 using 聲明

using 聲明 是前面帶 using 關鍵字的變數聲明。 它指示編譯器聲明的變數應在封閉範圍的末尾進行處理。 以下面編寫文本文件的程式碼為例:

static void WriteLinesToFile(IEnumerable<string> lines)  {      using var file = new System.IO.StreamWriter("WriteLines2.txt");      foreach (string line in lines)      {          // 如果該行不包含單詞「second」,則將該行寫入文件。          if (!line.Contains("Second"))          {              file.WriteLine(line);          }      }      // 文件已在此處釋放  }

在前面的示例中,當到達方法的右括弧時,將對該文件進行處理。 這是聲明 file 的範圍的末尾。 前面的程式碼相當於下面使用經典 using 語句語句的程式碼:

static void WriteLinesToFile(IEnumerable<string> lines)  {      using (var file = new System.IO.StreamWriter("WriteLines2.txt"))      {          foreach (string line in lines)          {              // 如果該行不包含單詞「second」,則將該行寫入文件。              if (!line.Contains("Second"))              {                  file.WriteLine(line);              }          }      } // 文件已在此處被釋放  }

在前面的示例中,當到達與 using 語句關聯的右括弧時,將對該文件進行處理。

在這兩種情況下,編譯器將生成對 Dispose() 的調用。 如果 using 語句中的表達式不可處置,編譯器將生成一個錯誤。

05 靜態本地函數

現在可以向本地函數添加 static 修飾符,以確保本地函數不會從封閉範圍捕獲(引用)任何變數。 這樣做會生成 CS8421,「靜態本地函數不能包含對 <variable> 的引用」。

考慮下列程式碼。 本地函數 LocalFunction 訪問在封閉範圍(方法 M)中聲明的變數 y。 因此,不能用 static 修飾符來聲明 LocalFunction

int M()  {      int y;      LocalFunction();      return y;        void LocalFunction() => y = 0;  }

下面的程式碼包含一個靜態本地函數。 它可以是靜態的,因為它不訪問封閉範圍中的任何變數:

int M()  {      int y = 5;      int x = 7;      return Add(x, y);        static int Add(int left, int right) => left + right;  }

06 可處置的 ref 結構

ref 修飾符聲明的 struct 可能無法實現任何介面,因此無法實現 IDisposable。 因此,要能夠處理 ref struct,它必須有一個可訪問的 void Dispose() 方法。 這同樣適用於 readonly ref struct 聲明。

07 可為空引用類型

在可為空注釋上下文中,引用類型的任何變數都被視為不可為空引用類型 。 若要指示一個變數可能為 null,必須在類型名稱後面附加 ?,以將該變數聲明為可為空引用類型 。

對於不可為空引用類型,編譯器使用流分析來確保在聲明時將本地變數初始化為非 Null 值。 欄位必須在構造過程中初始化。 如果沒有通過調用任何可用的構造函數或通過初始化表達式來設置變數,編譯器將生成警告。 此外,不能向不可為空引用類型分配一個可以為 Null 的值。

不對可為空引用類型進行檢查以確保它們沒有被賦予 Null 值或初始化為 Null。 不過,編譯器使用流分析來確保可為空引用類型的任何變數在被訪問或分配給不可為空引用類型之前,都會對其 Null 性進行檢查。

可以在可為空引用類型的概述中了解該功能的更多資訊。 可以在此可為空引用類型教程中的新應用程式中自行嘗試。 在遷移應用程式以使用可為空引用類型教程中了解遷移現有程式碼庫以使用可為空引用類型的步驟。

08 非同步流【*重要*】

從 C# 8.0 開始,可以創建並以非同步方式使用流。 返回非同步流的方法有三個屬性:

  1. 它是用 async 修飾符聲明的。
  2. 它將返回 IAsyncEnumerable<T>
  3. 該方法包含用於在非同步流中返回連續元素的 yield return 語句。

使用非同步流需要在枚舉流元素時在 foreach 關鍵字前面添加 await 關鍵字。 添加 await 關鍵字需要枚舉非同步流的方法,以使用 async 修飾符進行聲明並返回 async 方法允許的類型。 通常這意味著返回 TaskTask<TResult>。 也可以為 ValueTaskValueTask<TResult>。 方法既可以使用非同步流,也可以生成非同步流,這意味著它將返回 IAsyncEnumerable<T>。 下面的程式碼生成一個從 0 到 19 的序列,在生成每個數字之間等待 100 毫秒:

public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()  {      for (int i = 0; i < 20; i++)      {          await Task.Delay(100);          yield return i;      }  }

可以使用 await foreach 語句來枚舉序列:

await foreach (var number in GenerateSequence())  {      Console.WriteLine(number);  }

可以在創建和使用非同步流的教程中自行嘗試非同步流。

09 索引和範圍

範圍和索引為在數組中指定子範圍(Span<T>ReadOnlySpan<T>)提供了簡潔語法。

此語言支援依賴於兩個新類型和兩個新運算符。

  • System.Index 表示一個序列索引。
  • ^ 運算符,指定一個索引與序列末尾相關。
  • System.Range 表示序列的子範圍。
  • 範圍運算符 (..),用於指定範圍的開始和末尾,就像操作數一樣。

讓我們從索引規則開始。 請考慮數組 sequence0 索引與 sequence[0] 相同。 ^0 索引與 sequence[sequence.Length] 相同。 請注意,sequence[^0] 不會引發異常,就像 sequence[sequence.Length] 一樣。 對於任何數字 n,索引 ^nsequence.Length - n 相同。

範圍指定範圍的開始和末尾 。 包括此範圍的開始,但不包括此範圍的末尾,這表示此範圍包含開始但不包含末尾。 範圍 [0..^0] 表示整個範圍,就像 [0..sequence.Length] 表示整個範圍。

請看以下幾個示例。 請考慮以下數組,用其順數索引和倒數索引進行注釋:

var words = new string[]  {                  // index from start    index from end      "The",      // 0                   ^9      "quick",    // 1                   ^8      "brown",    // 2                   ^7      "fox",      // 3                   ^6      "jumped",   // 4                   ^5      "over",     // 5                   ^4      "the",      // 6                   ^3      "lazy",     // 7                   ^2      "dog"       // 8                   ^1  };              // 9 (or words.Length) ^0

可以使用 ^1 索引檢索最後一個詞:

Console.WriteLine($"The last word is {words[^1]}");  // writes "dog"

以下程式碼創建了一個包含單詞「quick」、「brown」和「fox」的子範圍。 它包括 words[1]words[3]。 元素 words[4] 不在此範圍內。

var quickBrownFox = words[1..4];

以下程式碼使用「lazy」和「dog」創建一個子範圍。 它包括 words[^2]words[^1]。 不包括結束索引 words[^0]

var lazyDog = words[^2..^0];

下面的示例為開始和/或結束創建了開放範圍:

var allWords = words[..];      // contains "The" through "dog".  var firstPhrase = words[..4];  // contains "The" through "fox"  var lastPhrase = words[6..];   // contains "the", "lazy" and "dog"

此外可以將範圍聲明為變數:

Range phrase = 1..4;

然後可以在 [] 字元中使用該範圍:

var text = words[phrase];

可在有關索引和範圍的教程中詳細了解索引和範圍。