當您閱讀本文時,C# 7 設計團隊已討論、規劃、實驗和計劃了大約一年之久。在本期中,我將介紹他們一直在探討的一些觀點示例。
在檢視時,請注意目前這些仍是要在 C# 7 中體現的觀點。雖然一些觀點只是經過了團隊討論階段,但另一些觀點已進入了實驗實現階段。無論如何,所有這些概念均尚未最終敲定;很多觀點可能會夭折;甚至是那些已經進入後期階段的觀點也可能會在最終確定語言的最後幾個階段被推翻。
宣告可以為 null 和不可以為 null 的引用型別
C# 7 討論中湧現的一些最重要的觀點也許與進一步改進 null 的處理方式有關,類似於 C# 6.0 的 null 條件運算子。其中最簡單的一項改進可能是,在執行編譯器或分析器驗證(訪問可以為 null 的型別例項)之前,先檢查型別實際上是否是不可以為 null 的型別。
如果需要不可以為 null 的引用型別,且您能夠完全避免 null,會怎樣? 此觀點旨在宣告引用型別是會允許 null (string?),還是會避免 null (string!)。從理論上講,甚至可以假定新程式碼中的所有引用型別宣告預設情況下都不可以為 null。然而,正如與我合著“必備 C# 6.0”一書的作者 Eric Lippert 所指出的,確保在編譯時引用型別永遠不為 null 極為困難 (bit.ly/1Rd5ekS)。
即便如此,還是可以確定型別可能為 null且尚未取消引用的的情況,而無需檢查是否是不可以為 null 的型別。或者,也可能發生以下情況:型別可能被分配為 null,儘管宣告意圖是分配不可以為 null 的型別。
為了擴大受益範圍,設計團隊正在討論能否對引數使用不可以為 null 的型別宣告,以便自動生成 null 檢查(儘管這可能會成為一項可選決定,以避免任何非預期的效能降低,除非可以在編譯時這樣做)。
(諷刺的是,C# 2.0 新增了可以為 null 的值型別,因為在很多情況下(如從資料庫中檢索的資料),有必要讓整數包含 null 值。現在,在 C# 7 中,設計團隊正在考慮支援相反的引用型別。)
對於不可以為 null 的型別(例如,string! 文字)的引用型別支援,另一個有趣考慮是公共中間語言 (CIL) 中的實現情況。兩個最常用的方案是將它對映到 NonNullable<T> 型別語法,或者像在 [Nullable] 字串文字中一樣利用屬性。後者是目前的首選方法。
元組
元組是設計團隊考慮為 C# 7 新增的另一項功能。此主題已在早期語言版本中多次被提出,但仍未予以落實。根據此觀點,可以在集合中宣告型別,這樣宣告中就能包含多個值;同樣地,方法也可以返回多個值。若要理解此概念,請檢視下面的示例程式碼:
1 2 3 4 5 6 7 8 |
public class Person { public readonly (string firstName, int lastName) Names; // a tuple public Person((string FirstName, string LastName)) names, int Age) { Names = names; } } |
如列表所示,藉助元組支援,您可以將型別宣告為元組,其中包含兩個或多個值。可以在使用資料型別的所有情景(包括欄位、引數、變數宣告或方法返回)中利用此功能。例如,下面的程式碼片段會從方法中返回元組:
1 2 3 4 5 6 7 8 9 10 11 |
public (string FirstName, string LastName) GetNames(string! fullName) { string[] names = fullName.Split(" ", 2); return (names[0], names[1]); } public void Main() { // ... (string first, string last) = GetNames("Inigo Montoya"); // ... } |
在此列表中,有返回元組的方法,以及 GetNames 結果被分配到的第一個和最後一個變數宣告。請注意,此分配是基於元組內的順序(而不是接收變數的名稱)。想想我們目前使用的一些替代方法(如陣列、集合、自定義型別或輸出引數),元組確實具有吸引力。
可以將許多選項與元組結合使用。下面介紹了一些審議選項:
- 元組可以有命名或未命名的屬性,如下所示:
1 |
var name = ("Inigo", "Montoya") |
和:
1 |
var name = (first: "John", last: "Doe") |
- 結果可以是匿名型別或顯式變數,如下所示:
1 |
var name = (first: "John", last: "Doe") |
或:
1 |
(string first, string last) = GetNames("Inigo Montoya") |
- 您可以將陣列轉換成元組,如下所示:
1 |
var names = new[]{ "Inigo", "Montoya" } |
- 您可以按名稱訪問各個元組項,如下所示:
1 |
Console.WriteLine($”My name is { names.first } { names.last }.”); |
- 可以推斷未明確指定的資料型別(大體上遵循匿名型別使用的相同方法)
儘管元組還有很多複雜之處,但在大多數情況下,元組遵循的是語言內架構完善的結構,所以它們可以強有力地支援 C# 7 中的功能。
模式匹配
模式匹配也是 C# 7 設計團隊經常討論的主題。或許,關於模式匹配的一種更易理解的呈現是,在 case 語句中支援表示式模式(而不僅僅是常量)的擴充套件 switch(和 if)語句。(若要與擴充套件 case 語句對應,switch 表示式型別不能侷限於擁有對應的常量值的型別)。
藉助模式匹配,您可以查詢模式的 switch 表示式。例如,您能夠查詢 switch 表示式是特定的型別、具有特定成員的型別,還是匹配特定“模式”或表示式的型別。例如,假設 obj 可能是 Point 型別,並且其 x 值大於 2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
object obj; // ... switch(obj) { case 42: // ... case Color.Red: // ... case string s: // ... case Point(int x, 42) where (Y > 42): // ... case Point(490, 42): // fine // ... default: // ... } |
有趣的是,當給定的表示式作為 case 語句時,也有必要允許表示式作為 goto case 語句上的自變數。
為了支援 Point 型別的 case 語句,Point 上必須有一些處理模式匹配的成員型別。在此示例中,需要可提取兩個 int 型別的自變數的成員。例如,成員:
1 |
public static bool operator is (Point self out int x, out int y) {...} |
請注意,如果沒有 where 表示式,case Point(490, 42) 可能永遠無法達到,進而會導致編譯器發出錯誤或警告。
switch 語句的限制因素之一是,它不返回值,而是執行程式碼塊。模式匹配的新增功能可以支援返回值的 switchexpression,如下所示:
1 |
string text = match (e) { pattern => expression; ... ; default => expression } |
同樣,is 運算子可支援模式匹配,不僅允許進行型別檢查,還支援就型別上是否存在特定成員進行更通用的查詢。
記錄
作為 C# 6.0 中考慮新增的簡化“建構函式”宣告語法(但最終遭到拒絕)的延續,支援在類定義中嵌入建構函式宣告,我們將這種概念稱為“記錄”。 例如,假設宣告如下:
1 |
class Person(string Name, int Age); |
此簡單語句會自動生成以下內容:
- 建構函式:
1 2 3 4 5 |
public Person(string Name, int Age) { this.Name = Name; this.Age = Age; } |
- 只讀屬性,從而建立不可變型別
- 等同性實現(如 GetHashCode、等於、運算子 ==、運算子 != 等)
- ToString 的預設實現
- “is”運算子的模式匹配支援
儘管會生成大量程式碼(考慮到僅僅一個很短的程式碼行就建立了它的全部),但我們希望可以為手動編碼(本質上是樣本實現)提供相應的重要快捷方式。此外,所有程式碼都可以被視為顯式實現中的“預設”程式碼,其中的任意內容將具有優先權,並阻止生成相同的成員。
與記錄有關的一個更棘手的問題是,如何處理序列化。相當典型的做法大概是將記錄用作資料傳輸物件 (DTO),但仍不明確如何(若有措施)支援此類記錄的序列化。
與記錄相關的是,支援 with 表示式。藉助 with 表示式,您可以根據現有物件對新物件進行例項化。以 person 物件宣告為例,您可以通過以下 with 表示式新建一個例項:
1 2 |
Person inigo = new Person("Inigo Montoya", 42); Person humperdink = inigo with { Name = "Prince Humperdink" }; |
生成的與 with 表示式對應的程式碼如下所示:
1 |
Person humperdink = new Person(Name: "Prince Humperdink", Age: inigo.42 ); |
不過,另一建議是與其依賴 with 表示式的建構函式簽名,更可取的做法是將它轉換成 with 方法的呼叫,如下所示:
1 |
Person humperdink = inigo.With(Name: "Prince Humperdink", Age: inigo.42); |
非同步流
為了加強 C# 7 中的非同步支援,處理非同步序列的概念非常新奇。以 IAsyncEnumerable 為例,它的屬性為 Current 且方法為 Task<bool> MoveNextAsync。您可以使用 foreach 迴圈訪問 IAsyncEnumerable 例項,並讓編譯器負責非同步呼叫流中的每個成員,即執行 await 以確定序列(可能是通道)中是否有要處理的另一元素。
對此,還有很多需要評估的注意事項;其中最不需要注意的是,所有返回 IAsyncEnumerable 的 LINQ 標準查詢運算子可能會出現的 LINQ 膨脹。此外,如何公開 CancellationToken 支援和 Task.ConfigureAwait 仍不確定。
命令列上的 C#
我熱衷於研究 Windows PowerShell 如何讓 Microsoft .NET Framework 可用於命令列介面 (CLI),我特別感興趣的一個方面(也許是我最喜歡的一項審議功能)是支援在命令列上使用 C#;通常將這個概念稱為支援讀取、求值、列印、迴圈 (REPL)。
正如人們所希望的一樣,REPL 支援會隨附 C# 指令碼功能,這在不繁瑣的簡單方案中不需要使用所有常見形式(如類宣告)。沒有編譯步驟,REPL 會需要新指令來引用程式集和 NuGet 包,以及匯入其他檔案。目前正在討論中的方案會支援:
- 用於引用其他程式集或 NuGet 包的 #r。變體是 #r!,它甚至允許訪問內部成員,儘管有一些約束。(這適用於您有要訪問的程式集的原始碼的情況。)
- 用於新增整個目錄的 #l(與 F# 類似)。
- 用於匯入其他 C# 指令碼檔案的 #load,方法與您在專案中新增指令碼檔案幾乎相同,不同之處在於現在順序很重要。(請注意,可能不支援匯入 .cs 檔案,因為不允許在 C# 指令碼中使用名稱空間。)
- 在執行的同時開啟效能診斷的 #time。
您可以期待即將與 Visual Studio 2015 Update 1 一同釋出的首版 C# REPL(以及支援相同功能集的已更新互動式視窗)。有關更多資訊,請訪問 Itl.tc/CSREPL,以及檢視我下個月的專欄。
總結
雖然有準備了一年的材料,但若要探究設計團隊的所有工作,我們還有其他太多資訊需要了解。即使是我介紹的那些觀點,您也還是需要考慮其他許多詳細資訊(注意事項和優勢)。不過,我希望您現在已經瞭解設計團隊一直在探討的觀點,以及他們正如何尋求改進已經非常出色的 C# 語言。如果您想直接查閱 C# 7 設計說明,並提供您自己的反饋意見,則可以跳轉到 bit.ly/CSharp7DesignNotes 進行討論。