C#7.0 新增功能

  • 2019 年 10 月 4 日
  • 筆記

C# 7.0 向 C# 語言添加了許多新功能

01 out 變量

支持 out 參數的現有語法已在此版本中得到改進。 現在可以在方法調用的參數列表中聲明 out 變量,而不是編寫單獨的聲明語句:

if (int.TryParse(input, out int result))      Console.WriteLine(result);  else      Console.WriteLine("Could not parse input");

為清晰明了,可能需指定 out 變量的類型,如上所示。 但是,該語言支持使用隱式類型的局部變量:

if (int.TryParse(input, out var answer))      Console.WriteLine(answer);  else      Console.WriteLine("Could not parse input");
  • 代碼更易於閱讀。
    • 在使用 out 變量的地方聲明 out 變量,而不是在上面的另一行。
  • 無需分配初始值。
    • 通過在方法調用中使用 out 變量的位置聲明該變量,使得在分配它之前不可能意外使用它。

02 元組

C# 為用於說明設計意圖的類和結構提供了豐富的語法。 但是,這種豐富的語法有時會需要額外的工作,但益處卻很少。 你可能經常編寫需要包含多個數據元素的簡單結構的方法。為了支持這些方案,已將元組 添加到了 C#。 元組是包含多個字段以表示數據成員的輕量級數據結構。 這些字段沒有經過驗證,並且你無法定義自己的方法

低於 C# 7.0 的版本中也提供元組,但它們效率低下且不具有語言支持。 這意味着元組元素只能作為 Item1 和 Item2 等引用。 C# 7.0 引入了對元組的語言支持,可利用更有效的新元組類型向元組字段賦予語義名稱。

可以通過為每個成員賦值來創建元組,並可選擇為元組的每個成員提供語義名稱:

(string Alpha, string Beta) namedLetters = ("a", "b");  Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

namedLetters 元組包含稱為 AlphaBeta 的字段。 這些名稱僅存在於編譯時且不保留,例如在運行時使用反射來檢查元組時。

在進行元組賦值時,還可以指定賦值右側的字段的名稱:

var alphabetStart = (Alpha: "a", Beta: "b");  Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");

在某些時候,你可能想要解包從方法返回的元組的成員。 可通過為元組中的每個值聲明單獨的變量來實現此目的。 這種解包操作稱為解構元組

(int max, int min) = Range(numbers);  Console.WriteLine(max);  Console.WriteLine(min);

還可以為 .NET 中的任何類型提供類似的析構。 編寫 Deconstruct 方法,用作類的成員。Deconstruct 方法為你要提取的每個屬性提供一組 out 參數。 考慮提供析構函數方法的此 Point 類,該方法提取 XY 坐標:

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

可以通過向元組分配 Point 來提取各個字段:

var p = new Point(3.14, 2.71);  (double X, double Y) = p;

可在元組相關文章中深入了解有關元組的詳細信息。

03 棄元

通常,在進行元組解構或使用 out 參數調用方法時,必須定義一個其值無關緊要且你不打算使用的變量。 為處理此情況,C# 增添了對棄元的支持 。 棄元是一個名為 _(下劃線字符)的只寫變量,可向單個變量賦予要放棄的所有值。 棄元類似於未賦值的變量;不可在代碼中使用棄元(賦值語句除外)。

在以下方案中支持棄元:

  • 在對元組或用戶定義的類型進行解構時。
  • 在使用 out 參數調用方法時。
  • 在使用 isswitch 語句匹配操作的模式中。
  • 在要將某賦值的值顯式標識為棄元時用作獨立標識符。

以下示例定義了 QueryCityDataForYears 方法,它返回一個包含兩個不同年份的城市數據的六元組。 本例中,方法調用僅與此方法返回的兩個人口值相關,因此在進行元組解構時,將元組中的其餘值視為棄元。

 1 using System;   2 using System.Collections.Generic;   3   4 public class Example   5 {   6    public static void Main()   7    {   8        var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);   9  10        Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");  11    }  12  13    private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)  14    {  15       int population1 = 0, population2 = 0;  16       double area = 0;  17  18       if (name == "New York City") {  19          area = 468.48;  20          if (year1 == 1960) {  21             population1 = 7781984;  22          }  23          if (year2 == 2010) {  24             population2 = 8175133;  25          }  26       return (name, area, year1, population1, year2, population2);  27       }  28  29       return ("", 0, 0, 0, 0, 0);  30    }  31 }  32 //  輸出結果:  33 //  Population change, 1960 to 2010: 393,149

有關詳細信息,請參閱棄元

04 模式匹配

模式匹配 是一種可讓你對除對象類型以外的屬性實現方法分派的功能。 你可能已經熟悉基於對象類型的方法分派。 在面向對象的編程中,虛擬和重寫方法提供語言語法來實現基於對象類型的方法分派。 基類和派生類提供不同的實現。 模式匹配表達式擴展了這一概念,以便你可以通過繼承層次結構為不相關的類型和數據元素輕鬆實現類似的分派模式。

模式匹配支持 is 表達式和 switch 表達式。 每個表達式都允許檢查對象及其屬性以確定該對象是否滿足所尋求的模式。 使用 when 關鍵字來指定模式的其他規則。

is 模式表達式擴展了常用 is 運算符以查詢關於其類型的對象,並在一條指令分配結果。以下代碼檢查變量是否為 int,如果是,則將其添加到當前總和:

if (input is int count)      sum += count;

前面的小型示例演示了 is 表達式的增強功能。 可以針對值類型和引用類型進行測試,並且可以將成功結果分配給類型正確的新變量。

switch 匹配表達式具有常見的語法,它基於已包含在 C# 語言中的 switch 語句。 更新後的 switch 語句有幾個新構造:

  • switch 表達式的控制類型不再局限於整數類型、Enum 類型、string 或與這些類型之一對應的可為 null 的類型。 可能會使用任何類型。
  • 可以在每個 case 標籤中測試 switch 表達式的類型。 與 is 表達式一樣,可以為該類型指定一個新變量。
  • 可以添加 when 子句以進一步測試該變量的條件。
  • case 標籤的順序現在很重要。 執行匹配的第一個分支;其他將跳過。

以下代碼演示了這些新功能:

public static int SumPositiveNumbers(IEnumerable<object> sequence)  {      int sum = 0;      foreach (var i in sequence)      {          switch (i)          {              case 0:                  break;              case IEnumerable<int> childSequence:              {                  foreach(var item in childSequence)                      sum += (item > 0) ? item : 0;                  break;              }              case int n when n > 0:                  sum += n;                  break;              case null:                  throw new NullReferenceException("Null found in sequence");              default:                  throw new InvalidOperationException("Unrecognized type");          }      }      return sum;  }
  • case 0: 是常見的常量模式。
  • case IEnumerable<int> childSequence: 是一種類型模式。
  • case int n when n > 0: 是具有附加 when 條件的類型模式。
  • case null: 是 null 模式。
  • default: 是常見的默認事例。

可以在 C# 中的模式匹配中了解有關模式匹配的更多信息。

05 Ref 局部變量和返回結果

此功能允許使用並返回對變量的引用的算法,這些變量在其他位置定義。 一個示例是使用大型矩陣並查找具有某些特徵的單個位置。 下面的方法在矩陣中向該存儲返回「引用」 :

    /// <summary>      ///  Ref局部變量和返回結果      /// </summary>      public class MatrixSearch      {          public static ref int Find(int[,] matrix, Func<int, bool> predicate)          {              for (int i = 0; i < matrix.GetLength(0); i++)              {                  for (int j = 0; j < matrix.GetLength(1); j++)                  {                      if (predicate(matrix[i, j]))                      {                          return ref matrix[i, j];                      }                  }              }              throw new InvalidOperationException("Not found");          }      }

可以將返回值聲明為 ref 並在矩陣中修改該值,如以下代碼所示:

 int[,] matrix = new int[5,6];   ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42);   Console.WriteLine(item);   item = 24;   Console.WriteLine(matrix[4, 2]);

C# 語言還有多個規則,可保護你免於誤用 ref 局部變量和返回結果:

  • 必須將 ref 關鍵字添加到方法簽名和方法中的所有 return 語句中。
    • 這清楚地表明,該方法在整個方法中通過引用返回。
  • 可以將 ref return 分配給值變量或 ref 變量。
    • 調用方控制是否複製返回值。 在分配返回值時省略 ref 修飾符表示調用方需要該值的副本,而不是對存儲的引用。
  • 不可向 ref 本地變量賦予標準方法返回值。
    • 因為那將禁止類似 ref int i = sequence.Count(); 這樣的語句
  • 不能將 ref 返回給其生存期不超出方法執行的變量。
    • 這意味着不可返回對本地變量或對類似作用域變量的引用。
  • ref 局部變量和返回結果不可用於異步方法。
    • 編譯器無法知道異步方法返回時,引用的變量是否已設置為其最終值。

添加 ref 局部變量和 ref 返回結果可通過避免複製值或多次執行取消引用操作,允許更為高效的算法。

向返回值添加 ref源兼容的更改。 現有代碼會進行編譯,但在分配時複製 ref 返回值。調用方必須將存儲的返回值更新為 ref 局部變量,從而將返回值存儲為引用。

有關詳細信息,請參閱 ref 關鍵字一文。

06 本地函數

許多類的設計都包括僅從一個位置調用的方法。 這些額外的私有方法使每個方法保持小且集中。 本地函數使你能夠在另一個方法的上下文內聲明方法 。 本地函數使得類的閱讀者更容易看到本地方法僅從聲明它的上下文中調用。

對於本地函數有兩個常見的用例:公共迭代器方法和公共異步方法。 這兩種類型的方法都生成報告錯誤的時間晚於程序員期望時間的代碼。 在迭代器方法中,只有在調用枚舉返回的序列的代碼時才會觀察到任何異常。 在異步方法中,只有當返回的 Task 處於等待狀態時才會觀察到任何異常。 以下示例演示如何使用本地函數將參數驗證與迭代器實現分離:

 1  public static IEnumerable<char> AlphabetSubset3(char start, char end)   2         {   3             if (start < 'a' || start > 'z')   4                 throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");   5             if (end < 'a' || end > 'z')   6                 throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");   7   8             if (end <= start)   9                 throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");  10  11             return AlphabetSubsetImplementation();  12  13             IEnumerable<char> AlphabetSubsetImplementation()  14             {  15                 for (var c = start; c < end; c++)  16                 {  17                     yield return c;  18                 }  19             }  20         }

可以對 async 方法採用相同的技術,以確保在異步工作開始之前引發由參數驗證引起的異常:

 1  public Task<string> PerformLongRunningWork(string address, int index, string name)   2         {   3             if (string.IsNullOrWhiteSpace(address))   4                 throw new ArgumentException(message: "An address is required", paramName: nameof(address));   5             if (index < 0)   6                 throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");   7             if (string.IsNullOrWhiteSpace(name))   8                 throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));   9  10             return LongRunningWorkImplementation();  11  12             async Task<string> LongRunningWorkImplementation()  13             {  14                 var interimResult = await FirstWork(address);  15                 var secondResult = await SecondStep(index, name);  16                 return $"The results are {interimResult} and {secondResult}. Enjoy.";  17             }  18         }  19  20         private async Task<string> FirstWork(string address)  21         {  22             // await  ··· 業務邏輯  23             return "";  24         }  25  26         private async Task<string> SecondStep(int index, string name)  27         {  28             // await  ··· 業務邏輯  29             return "";  30         }

本地函數支持的某些設計也可以使用 lambda 表達式 來完成。 感興趣的可以閱讀有關差異的詳細信息

07 更多的 expression-bodied 成員

C# 6 為成員函數和只讀屬性引入了 expression-bodied 成員。 C# 7.0 擴展了可作為表達式實現的允許的成員。 在 C# 7.0 中,你可以在屬性 和索引器 上實現構造函數 、終結器 以及 getset 訪問器。 以下代碼演示了每種情況的示例:

 public class ExpressionMembersExample      {          // Expression-bodied 構造函          public ExpressionMembersExample(string label) => this.Label = label;            // Expression-bodied 終結器          ~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");            private string label;            // Expression-bodied get / set          public string Label          {              get => label;              set => this.label = value ?? "Default label";          }      }

本示例不需要終結器,但顯示它是為了演示語法。 不應在類中實現終結器,除非有必要發佈非託管資源。 還應考慮使用 SafeHandle 類,而不是直接管理非託管資源。

這些 expression-bodied 成員的新位置代表了 C# 語言的一個重要里程碑:這些功能由致力於開發開放源代碼 Roslyn 項目的社區成員實現。

將方法更改為 expression bodied 成員是二進制兼容的更改

08 引發表達式

在 C# 中,throw 始終是一個語句。 因為 throw 是一個語句而非表達式,所以在某些 C# 構造中無法使用它。 它們包括條件表達式、null 合併表達式和一些 lambda 表達式。 添加 expression-bodied 成員將添加更多位置,在這些位置中,throw 表達式會很有用。 為了可以編寫這些構造,C# 7.0 引入了 throw 表達式。這使得編寫更多基於表達式的代碼變得更容易。 不需要其他語句來進行錯誤檢查。

從 C# 7.0 開始,throw 可以用作表達式和語句。 這允許在以前不支持的上下文中引發異常。 這些方法包括:

  • 條件運算符。 下例使用 throw 表達式在向方法傳遞空字符串數組時引發 ArgumentException。 在 C# 7.0 之前,此邏輯將需要顯示在 if/else 語句中。
 private static void DisplayFirstNumber(string[] args)          {              string arg = args.Length >= 1 ? args[0] : throw new ArgumentException("You must supply an argument");                if (Int64.TryParse(arg, out var number))              {                  Console.WriteLine($"You entered {number:F0}");              }              else              {                  Console.WriteLine($"{arg} is not a number.");              }            }
  • null 合併運算符。 在以下示例中,如果分配給 Name 屬性的字符串為 null,則將 throw 表達式與 null 合併運算符結合使用以引發異常。
public string Name          {              get => name;              set => name = value ??                            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");          }
DateTime ToDateTime(IFormatProvider provider) =>           throw new InvalidCastException("Conversion to a DateTime is not supported.");

09 通用的異步返回類型

從異步方法返回 Task 對象可能在某些路徑中導致性能瓶頸。 Task 是引用類型,因此使用它意味着分配對象。 如果使用 async 修飾符聲明的方法返回緩存結果或以同步方式完成,那麼額外的分配在代碼的性能關鍵部分可能要耗費相當長的時間。 如果這些分配發生在緊湊循環中,則成本會變高。

新語言功能意味着異步方法返回類型不限於 TaskTask<T>void。 返回類型必須仍滿足異步模式,這意味着 GetAwaiter 方法必須是可訪問的。 作為一個具體示例,已將 ValueTask 類型添加到 .NET framework 中,以使用這一新語言功能:

 public async ValueTask<int> Func()   {      await Task.Delay(100);      return 5;   }

需要添加 NuGet 包 System.Threading.Tasks.Extensions 才能使用 ValueTask 類型。

此增強功能對於庫作者最有用,可避免在性能關鍵型代碼中分配 Task

10 數字文本語法改進

誤讀的數值常量可能使第一次閱讀代碼時更難理解。 位掩碼或其他符號值容易產生誤解。C# 7.0 包括兩項新功能,可用於以最可讀的方式寫入數字來用於預期用途:二進制文本和數字分隔符 。

在創建位掩碼時,或每當數字的二進制表示形式使代碼最具可讀性時,以二進制形式寫入該數字:

public const int Sixteen =   0b0001_0000;  public const int ThirtyTwo = 0b0010_0000;  public const int SixtyFour = 0b0100_0000;  public const int OneHundredTwentyEight = 0b1000_0000;

常量開頭的 0b 表示該數字以二進制數形式寫入。 二進制數可能會很長,因此通過引入 _作為數字分隔符通常更易於查看位模式,如上面二進制常量所示。 數字分隔符可以出現在常量的任何位置。 對於十進制數字,通常將其用作千位分隔符:

public const long BillionsAndBillions = 100_000_000_000;

數字分隔符也可以與 decimalfloatdouble 類型一起使用:

public const double  AvogadroConstant = 6.022_140_857_747_474e23;  public const decimal GoldenRatio =      1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;

綜觀來說,你可以聲明可讀性更強的數值常量。