原文:What's new in C# 7
2016-12-21c++譯者注:原文於 2016 年 12 月發表,當時 Visual Studio 2017 仍是 15 Preview 5,不過直到 VS2017 它們仍然沒什麼變化。git
C# 7 中添加了很多語言特性:程序員
out
變量github
out
變量元組(Tuples)算法
模式匹配express
定義爲 ref
的局部變量和返回值編程
局部函數api
更多成員可以使用表達式語法緩存
throw
表達式安全
異步返回類型泛型化
async
定義的方法能夠返回除 Task
和 Task<T>
以外的類型了。改善數字字符量語法
本文下面對每一個特性都進行了詳述。你能夠了解到每一個特性背後的緣由,也能夠學習到相關的語法。還有一些使用這些特性的場景示例。全部這些都會讓你成爲更有高效的開發者。
out
變量現有的語法已經支持 out
參數,但它在這個版本中獲得了改進。
以前,你須要在兩個語句中申明和使用輸出(out)變量:
int numericResult; if (int.TryParse(input, out numericResult)) WriteLine(numericResult); else WriteLine("Could not parse input");
而如今能夠在調用方法的時候直接在參數列表中定義 out
變量,避免單獨的申明語句:
if (int.TryParse(input, out int result)) WriteLine(result); else WriteLine("Could not parse input");
你能夠像上面那樣明確的申明 out
變量的類型,不過語言自己支持對局部變量使用隱式類型(自動推斷):
if (int.TryParse(input, out var answer)) WriteLine(answer); else WriteLine("Could not parse input");
這個代碼很是易讀
不須要賦予初始值
out
變量的時候才申明它,就不會出現尚未賦值就使用的狀況。這個特性最經常使用的地方是在使用 try
模式的時候。在這個模式中,方法會返回 bool
值來標標識它是成功仍是失敗,若是成功,其處理的結果則是經過 out
變量提供的。
C# 提供了豐富語法來支持類和結構,而類和結構主要用於解釋你的設計思想。然而有時候,豐富的語法也須要一些額外的工做所帶來的小優點。你可能常常會在寫到某個方法時發現須要一個簡單但又包含多個數據元素的結構。爲了支持這類狀況,C# 添加了元組。元組是輕量級的數據結構,包含多個字段來表示數據成員。這些字段不通過驗證。另外,你也不能在元組中定義本身的方法。
注:在 C# 7 以前,元組已經經過 API 實現,但這個實現有不少限制。最重要的是,元組的成員被命名爲
Item1
、Item2
等(譯者注:語義不明)。語言(譯者注:指 C# 7 支持爲元組的字段定義語義化化的名稱。
能夠經過對元組的每一個成員賦值來定義元組:
var letters = ("a", "b");
這個賦值語句經過元組語法建立了一個元組,其成員是 Item1
和 Item2
。能夠修改這個賦值語句,使元組具備語義化的成員:
(string Alpha, string Beta) namedLetters = ("a", "b");
注:新的元組特性須要
System.ValueTuple
類型。在 Visual Studio 2017 和以前的預覽版本中,你須要添加 NuGet 包System.ValueTuple
。
namedLatters
元組包含 Alpha
和 Beta
兩個字段。在元組賦值過語句中,你也能夠在右側指定字段的名稱:
var alphabetStart = (Alpha: "a", Beta: "b");
C# 語言容許你在賦值語句的左側和右側同時指定字段名稱:
(string First, string Second) firstLetters = (Alpha: "a", Beta: "b");
上面這一行會產生一個警告,CS8123
,告訴你賦值語句右側指定的 Alpha
和 Beta
這兩個名稱會被忽略,由於它們與左側指定的 First
和 Second
名稱產生了衝突。
上面的例子展現了基本的元組語法。元組一般用於 private
和 internal
方法的返回類型。元組爲這些方法提供了簡單的語法來返回多個數值:再也不須要爲返回數據定義 class
或 struct
類型的工做,很省事。
建立元組更有效率。這是用來定義多值數據的一個簡單而輕量的語法。下面做爲示例的方法找到並返回一組整數的最小值和最大值:
private static (int Max, int Min) Range(IEnumerable<int> numbers) { int min = int.MaxValue; int max = int.MinValue; foreach(var n in numbers) { min = (n < min) ? n : min; max = (n > max) ? n : max; } return (max, min); }
這裏使用元組帶來了以下優點:
class
或 struct
Create<T1>(T1)
方法方法申明中爲返回元組數據的字段提供了名稱。調用這個方法的時候,返回的元組會有 Max
和 Min
兩個字段:
var range = Range(numbers);
有時候你可能想將方法返回的元組數據拆解開。沒問題,你能夠爲元組的每一個字段單獨申明變量。這稱爲對元組進行解構(deconstructing):
(int max, int min) = Range(numbers);
你也能夠爲 .NET 中任意類類型提供相似的解構功能,方法是爲類提供一個 Deconstruct
成員方法(譯者注:稱爲解構方法,注意與析構方法區分)。Deconstruct
方法提供一組 out
參數,對應於你想解構出來的每個屬性。下面的 Point
類提供了一個解構方法用於提取 X
和 Y
座標:
public class Point { public Point(double x, double y) { this.X = x; this.Y = y; } public double X { get; } public double Y { get; } public void Deconstruct(out double x, out double y) { x = this.X; y = this.Y; } }
如今能夠經過將 Point
對象賦值到元組來提取各個字段:
var p = new Point(3.14, 2.71); (double X, double Y) = p;
定義 Deconstruct
方法的時候並無綁定名稱。你能夠在賦值語句中提取變量時對其命名:
(double horizontalDistance, double verticalDistance) = p;
更多相關內容,請參閱元組主題
模式匹配功能讓你能夠在方法中根據對象類型以外的屬性進行處理。你可能已經很熟基於對象類型的方法處理。在面向對象編程中,虛方法和重載方法語法用於實現基於對象類型的方法處理。基類和派生類會提供不一樣的實現。模式匹配語法擴展了這個概念,讓你很容易根據類型和數據元素實現相似的處理模式,而這與繼承無關。
模式匹配支持 is
表達式和 switch
表達式。它經過對對象及其屬性的檢查來肯定對象是否知足所要求的模式。使用 when
關鍵字來爲模式指定附加規則。
is
表達式is
模式表達式擴展了你們熟悉的 is
運算符,用不只限於類型的方式來查詢對象。
讓咱們從一個簡單的問題開始。咱們會擴展這個問題來演示模式匹配是如何簡便處理問題的。首先,咱們來計算一個擲骰結果的數值之和。
public static int DiceSum(IEnumerable<int> values) { return values.Sum(); }
很快你發現須要統計的結果擲骰列表中,有些時候並非只擲了一個骰子。輸入的每一項都有多是多個結果,而不只僅是一個數:
public static int DiceSum2(IEnumerable<object> values) { var sum = 0; foreach(var item in values) { if (item is int val) sum += val; else if (item is IEnumerable<object> subList) sum += DiceSum2(subList); } return sum; }
這裏 is
模式表達式很好的發揮了做用。在檢查某一項的類型時,能夠同時進行變量初始化。這裏建立了一個有效的運行時類型變量。
繼續擴展這個示例中的問題,你可能會發現須要更多 if
和 else if
語句。這樣一來,你會想使用 switch
模式表達式。
switch
語句這個匹配表達式與 C# 語言中已經存在的 switch
語句具備類似的語法。在添加新的條件以前,先把上面的代碼轉換成匹配表達式:
public static int DiceSum3(IEnumerable<object> values) { var sum = 0; foreach (var item in values) { switch (item) { case int val: sum += val; break; case IEnumerable<object> subList: sum += DiceSum3(subList); break; } } return sum; }
匹配表達式的語法與 is
表達式略有不一樣,是在 case
表達式的開始位置申明類型和變量。
匹配表達式也支持常量,這在遇到簡單的條件判斷時會節約很多時間:
public static int DiceSum4(IEnumerable<object> values) { var sum = 0; foreach (var item in values) { switch (item) { case 0: break; case int val: sum += val; break; case IEnumerable<object> subList when subList.Any(): sum += DiceSum4(subList); break; case IEnumerable<object> subList: break; case null: break; default: throw new InvalidOperationException("unknown item type"); } } return sum; }
上面的代碼添加了 0
做爲 int
的特殊狀況,null
則是另外一個特殊狀況,表明沒有輸入。這演示了 switch 模式表達式中一項重要特性:須要注意 case
表達式的順序。0
這個條件必須出如今其它 int
條件以前。要否則,int
條件會先匹配到,即便值爲 0
。若是你搞錯了匹配表達式的順序,一個本應該後匹配到的條件被提早處理了,編譯器會標記出來併產生一個錯誤。
在處理空輸入的時候也存在相似的狀況。你能夠看到,特定 IEnumerable
的分支必須出如今通常 IEnumerable
的分支以前。
這一版本的代碼還添加了 default
分支。無論 default
放在源碼中什麼位置,它老是在最後進行判斷。所以,通常約定把 default
分支放在最後。
最後,咱們來添加最後一個 case
,用於處理咱們在遊戲加入的一種新骰子。某些遊戲使用百分骰來表示較大範圍的數。
注:兩個 10 面的百分骰能夠表示從 0 到 99 的每個數。一個骰子各面標記着
00
、10
、20
、...、90
,另外一個而標記着0
、1
、2
、...、9
。把兩個骰子的數值加起來就能獲得一個 0 到 99 之間的數。
爲了在集合中添加這種類型的骰子,須要先定義對應的類型:
public struct PercentileDie { public int Value { get; } public int Multiplier { get; } public PercentileDie(int multiplier, int value) { this.Value = value; this.Multiplier = multiplier; } }
而後,添加 case
匹配表達式來處理這種新類型:
public static int DiceSum5(IEnumerable<object> values) { var sum = 0; foreach (var item in values) { switch (item) { case 0: break; case int val: sum += val; break; case PercentileDie die: sum += die.Multiplier * die.Value; break; case IEnumerable<object> subList when subList.Any(): sum += DiceSum5(subList); break; case IEnumerable<object> subList: break; case null: break; default: throw new InvalidOperationException("unknown item type"); } } return sum; }
在基於對象類型和其它屬性來處理算法的時候,新的模式匹配表達式語法更簡間明瞭。模式匹配表達式經過數據類型來組織代碼代碼,並且與繼承無關。
若是想了解更多關於模式匹配的主題,請參閱 C# 中的模式匹配
這個語法特性容許使用和返回定義在其它地方的變量引用。一個示例是關於大型矩陣,須要在其中找到某個特定數據的位置。這裏定義一個方法返回矩陣中用來表示某個位置的兩個索引:
public static (int i, int j) 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 (i, j); return (-1, -1); // Not found }
這個代碼中有不少問題。首先,它是一返回元組的公共方法,雖然從語法上來講沒有問題,但對於公共 API 來講最好是使用用戶定義的類型(class 或 struct)。
其次,這個方法返回了矩陣中某項的索引,調用者能夠經過這對索引引用矩陣中的元素,並修改其值。
var indices = MatrixSearch.Find(matrix, (val) => val == 42); Console.WriteLine(indices); matrix[indices.i, indices.j] = 24;
相比之下你可能更願意寫一個返回矩陣元素引用的方法來改變元素的值。在之前,你只能使用不安全的代碼返回整數指針來實現。
讓咱們來經過一系列的變化演示引用局部變量的特性,並展現如何建立一個方法來返回內部存儲的引用。經過這些變化你會學習到返回引用和局部引用特性的規則,避免不當心對這個特性進行濫用。
咱們從修改 find
的返回類型爲 ref int
開始。而後修改返回語句,使其返回存儲於矩陣中一個值而不是它的一對索引:
// 注意這段代碼不能經過編譯。 // 方法申明爲返回引用類型, // 但返回語句返回的是一個特定值。 public static ref int Find2(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 matrix[i, j]; throw new InvalidOperationException("Not found"); }
你在申明方法返回 ref
變量的時候,你必須在全部返回語句中添加 ref
關鍵字,這表示返回的是引用,這有助於開發者在閱讀這段代碼時能清楚的知道返回的是引用。
public static ref int Find3(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"); }
如今這個方法返回矩陣某個整數值的引用,你須要在調用的位置對它進行修改。var
申明知道 varItem
如今是 int
而不是元組:
var valItem = MatrixSearch.Find3(matrix, (val) => val == 42); Console.WriteLine(valItem); valItem = 24; Console.WriteLine(matrix[4, 2]);
上例中第二句 WriteLine
語句輸出的值是 42
而不是 24
。變量 varItem
是 int
,而不是 ref int
。var
關鍵字會讓編譯器搞明白類型,但它並不會隱式地添加 ref
修飾符。由於這個變量不是 ref
變量,因此 ref return
將變量的值拷貝到了賦值語句的左側。
ref var item = ref MatrixSearch.Find3(matrix, (val) => val == 42); Console.WriteLine(item); item = 24; Console.WriteLine(matrix[4, 2]);
如今第二句 WriteLine
語句會打印出 24
,這表示矩陣中保存的內容已經被修改了。使用 ref
修飾符申明的局部變量能夠用來獲取 ref
返回。你必須在 ref
變量申明的時候對其進行初始化,不能將申明語句和初始化語句分開。
C# 語言還有兩項規則來避免你誤用 ref
局部變量和 ref
返回:
不能賦值給 ref 變量
ref int i = sequence.Count();
不能返回一個生命週期短於方法執行時間的 ref
變量。
這些規則確保您不會意外地混合值變量和引用變量,還確保你不能引用即將被垃圾回收的數據。
此外,局部引用和返回引用避免了在算法中拷貝值,或者進行屢次解引用操做,因此有利於提升效率。
不少類設計中都存在只在某一個地方調用的方法。這些額外的私有方法使方法變得小而專一。然而,它們同時也使閱讀類代碼變得困難。這些方法必須在調用上下文以外進行理解。
對於這些設計,局部函數容許你在某個方法的上下文內申明另外一個方法。局部方法使讀者更容易看到調用它的上下文。
局部方法頗有兩個很常見的用法:公共迭代方法和公共異步方法。這兩種類型的方法生成的代碼都會晚於程序員預期的時間報告錯誤。在迭代方法中,異常只會在枚舉調用的時候被發現。而異步方法中,只有其返回的任務完成才能觀察到異常發生。
先來看一個迭代方法:
public static IEnumerable<char> AlphabetSubset(char start, char end) { if ((start < 'a') || (start > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); if ((end < 'a') || (end > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); if (end <= start) throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); for (var c = start; c < end; c++) yield return c; }
檢查下面的代碼對迭代方法地錯誤調用:
var resultSet = Iterator.AlphabetSubset('f', 'a'); Console.WriteLine("iterator created"); foreach (var thing in resultSet) Console.Write($"{thing}, ");
異常會在 resultSet
迭代時拋出,而不是在 resultSet
建立出來的時候。在這個示例中,多數開發者能迅速診斷出問題所在。然而,在更大的代碼庫中,建立迭代器的代碼一般並不與枚舉其結果的代碼放在一塊兒。你能夠重構代碼讓公共方法驗證全部參數,再用私有方法來產生枚舉項:
public static IEnumerable<char> AlphabetSubset2(char start, char end) { if ((start < 'a') || (start > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); if ((end < 'a') || (end > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); if (end <= start) throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); return alphabetSubsetImplementation(start, end); } private static IEnumerable<char> alphabetSubsetImplementation(char start, char end) { for (var c = start; c < end; c++) yield return c; }
這個重構版本會即時拋出異常,由於公共方法並不是迭代方法;只有私有方法使用了 yield return
語法。然而,這個重構存潛在的問題。私有方法應該只由公共接口方法調用,由於它跳過了全部參數驗證。閱讀這個類的人必須從整個類中去發現這個事實,並查找在其它地方是否存在對 alphabetSubsetImplementation
方法的引用。
若是把 alphabetSubsetImplementation
定義爲公共 API 方法中的一個局部函數,就清楚多了:
public static IEnumerable<char> AlphabetSubset3(char start, char end) { if ((start < 'a') || (start > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); if ((end < 'a') || (end > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); if (end <= start) throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); return alphabetSubsetImplementation(); IEnumerable<char> alphabetSubsetImplementation() { for (var c = start; c < end; c++) yield return c; } }
上面這個版本很清晰的代表了局部方法只在外部方法的做用域內被引用。局部函數也能確保開發者不會意外地從類中其它位置調用局部函數以跳過參數驗證。
async
方法中也可使用一樣的技術來保證在實際工做以前進行參數驗證,並當即拋出異常:
public Task<string> PerformLongRunningWork(string address, int index, string name) { if (string.IsNullOrWhiteSpace(address)) throw new ArgumentException(message: "An address is required", paramName: nameof(address)); if (index < 0) throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException(message: "You must supply a name", paramName: nameof(name)); return longRunningWorkImplementation(); async Task<string> longRunningWorkImplementation() { var interimResult = await FirstWork(address); var secondResult = await SecondStep(index, name); return $"The results are {interimResult} and {secondResult}. Enjoy."; } }
注:某些設計使用 Lambda 表達式 來做爲局部函數。有興趣的朋友能夠去看看它們之間的區別
C# 6 對成員函數和只讀屬性引入了使用表達式做爲函數體的成員。C# 7 擴展了容許使用這一特性的成員。在 C# 7 中,能夠對構造方法、析構方法、屬性的 get
和 set
訪問器以及索引使用這一特性,下面是示例:
// Expression-bodied constructor public ExpressionMembersExample(string label) => this.Label = label; // Expression-bodied finalizer ~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!"); private string label; // Expression-bodied get / set accessors. public string Label { get => label; set => this.label = value ?? "Default label"; }
注:這個示例不須要析構方法,但它證實了這個語法有效。通常狀況下你不該該實現類的析構方法,除非必須在其中釋放非託管資源。另外,你應該考慮使用 SafeHandle 類來管理非託管資源,而不是直接對其進行管理。
這些支持表達式函數體的新成員標誌着 C# 語言一個重要的里程碑:這些特性由社區成員在開源的 Roslyn 項目實現。
## throw 表達式
C# 中 throw
曾經一直都是一個語句。由於 throw
是語句而不是表達式,有些地方就不能使用它。這些地方包括條件表達式、空值合併表達式以及一些 Lambda 表達式。throw 表達式做爲新增的表達式成員無疑是有益的。至此你能夠將 C# 7 引入的 throw 表達式寫在任意結構中。
它的語法和以前用到的 throw
語句語法類似。惟一不一樣的是你能夠把它用於一些新的位置,好比,條件表達式:
public string Name { get => name; set => name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "New name must not be null"); }
這個特性也容許在初始化表達式中使用 throw 表達式:
private ConfigResource loadedConfig = LoadConfigResourceOrDefault() ?? throw new InvalidOperationException("Could not load config");
而在之前,這些初始化過程都須要在構造方法中進行,在函數體中使用 throw 語句:
public ApplicationOptions() { loadedConfig = LoadConfigResourceOrDefault(); if (loadedConfig == null) throw new InvalidOperationException("Could not load config"); }
注:上述兩種結構都會在構造對象的時候拋出異常,這一般難以恢復。所以,應該儘可能不要設計在構造過程當中拋出異常。
從 async 方法中返回 Task
對象可能引發某些路徑的性能瓶頸。Task
是引用類型,因此使用它就意味着會分配對象。申明爲 async
的方法可能返回一個緩存的結果,或完成同步,這種狀況下,額外的分配會成爲重要的時間成本,這段代碼對性能相當重要。若是這些分配發生頻繁,它會變得很是昂貴。
新的語言特性容許 async 方法返回 Task
、Task<T>
和 void
以外的研。返回類型仍然必須知足 async 模式,即必需要有可訪問的 GetAwaiter
方法。ValueTask
做爲一個具體的示例已經添加到 .NET 框架中:
public async ValueTask<int> Func() { await Task.Delay(100); return 5; }
注:你須要添加 NuGet 包
System.Threading.Tasks.Extensions
以後才能在 Visual Studio 2017 中使用ValueTask
。
在以前使用 Task
的地方使用 ValueTask
是種簡單地優化。然而,若是你想手工進行更多優化,你能夠緩存異步操做返回的結果並之後面的調用使用它。ValueTask
結構體有一個使用 Task
做爲參數的構造函數,因此你能夠從現有 async 方法的返回結果構造 ValueTask
:
public ValueTask<int> CachedFunc() { return (cache) ? new ValueTask<int>(cacheResult) : new ValueTask<int>(loadCache()); } private bool cache = false; private int cacheResult; private async Task<int> loadCache() { // simulate async work: await Task.Delay(100); cache = true; cacheResult = 100; return cacheResult; }
與全部性能建議同樣,你應該在對代碼進行大規則更改以前對兩個版本進行基線測試。
誤讀數值會使用閱讀代碼變得困難。一個數在做爲二進制掩碼或其它符號,而不是做爲數值的時候,這種狀況常常發生。C# 7 引入了兩個新的特性來改善這類狀況,讓代碼更時尚也更容易閱讀:二進制字面量,數字分隔符。
建立二進制掩碼的時候,或須要提供二進制數值的時候,爲了代碼更易讀,能夠直接寫二進制字面量:
public const int One = 0b0001; public const int Two = 0b0010; public const int Four = 0b0100; public const int Eight = 0b1000;
0b
開始的常量表示它們被寫做二進制數。
二進制數可能會很長,因此引入 _
做爲數字分隔符:
public const int Sixteen = 0b0001_0000; public const int ThirtyTwo = 0b0010_0000; public const int SixtyFour = 0b0100_0000; public const int OneHundredTwentyEight = 0b1000_0000;
數字分隔符能夠出如今常量中任何地方。對於 10 進制數來講,它們經常使用做千分位分隔符:
public const long BillionsAndBillions = 100_000_000_000;
數字分隔符也可用於 decimal
、float
和 double
:
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;
使用上述兩個新特性,你能夠申明更易讀的數字常量。