一覽 C# 7.0 中的新功能
以下要帶來的是所有在 C# 7.0 中計畫的語言功能介紹。有了 Visual Studio “15” Preview 4 的釋出,大多數的功能都將可以使用了。現在就是一個絕佳的時機來嚐嚐鮮並告訴我們您的想法!
C# 7.0 加入不少新的功能並注重在資料取用、簡化程式碼與效能。或許最棒的特性是 tuple,它讓有多返回值更加簡單,與模式匹配,簡化大量的條件程式碼。但還有很多其他大大小小的功能,我們希望它們全都可以結合來讓您的程式碼更加有效率與簡潔,並讓你工作起來更愉快更有生產力。請使用在 Visual Studio 視窗上方的「傳送意見反應」按鈕來告訴我們什麼東西運作不如您的預期,或您對於功能的改善有想法。仍然還有一些東西沒辦法完整的運作在 Preview 4。下面我要介紹的功能是當最終版本釋出時預計可以運行的,而在 Note 裡面所提到的則是尚未如預期運行的東西。我也會介紹一些更改的計畫 - 值得一提的是,有些結果是來自於各位的反饋! 有些功能可能會更動或消失在最終釋出的版本。
如果您好奇有關這些功能的設計過程,您可以看到很多設計的筆記與其他討論在 Roslyn GitHub site。
享受 C# 7.0 的樂趣吧!Happy Hacking!
Out 變數
目前在 C# 使用 out 參數並不如我們想像中流暢。您要先宣告變數傳入才可以呼叫一個有 out 參數的方法。因為您通常不會初始化這些變數(畢竟它們之後也會被方法覆寫),所以您也不能用 var
來宣告他們,需要指定完整的類型:
public void PrintCoordinates(Point p)
{
int x, y; // have to "predeclare"
p.GetCoordinates(out x, out y);
WriteLine($"({x}, {y})");
}
在 C# 7.0 我們加入 out
變數;能夠直接宣告一個變數在它要傳入的地方,當成一個 out 的引數:
public void PrintCoordinates(Point p)
{
p.GetCoordinates(out int x, out int y);
WriteLine($"({x}, {y})");
}
要注意變數在封閉區塊中的作用範圍中,這樣隨後的行就可以使用它們。大多數的陳述式並沒有建立它們自己的作用範圍,所以 out
變數宣告在它們裡面通常會引進到封閉的範圍。
Note:在 Preview 4,作用範圍的規定有更多限制:out 變數作用在它們宣告的陳述式中。因此,上面的例子並不能使用直到之後的版本釋出。
因為 out
變數直接宣告為對 out
參數的引數,編譯器通常可以區別它們的類型(除非有衝突的多載),所以是可以用 var
代替一個類型來定義它們:
p.GetCoordinates(out var x, out var y);
一個常見的 out
參數使用就是 Try…
模式,會有一個布林的回傳值表示成功,而 out
參數將會攜帶所得到的結果:
public void PrintStars(string s)
{
if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); }
else { WriteLine("Cloudy - no stars tonight!"); }
}
Note:這裡的 i 只用在定義它的這個 if 陳述式中,所以 Preview 4 可以處理這種情況。
我們計畫允許「萬用字元」也可以當成 out 參數,用 * 表示,讓您可以忽略您不在乎的 out 參數:
p.GetCoordinates(out int x, out *); // I only care about x
Note:仍然未確定萬用字元會不會在 C# 7.0 版本中。
模式匹配
C# 7.0 引入模式的概念,抽象地說就是語法元素,能用來測試一個資料是否具有某種"型態",並在被運用的時候從值裏頭獲取的額外資訊。
在 C# 7.0 中模式的例子:
- 常數模式,以
c
表示(在 C# 中c
是常數的表達式),測試輸入是否等於c
。 - 類型模式,以
T x
表示(T
為類型而x
為識別項),測試輸入是否為類型T
,如果是的話就把輸入的值丟入類型為T
的變數x
中。 - Var 模式,以
var x
表示(x
為識別項<code),這種情況總是會匹配成功,所以就只是把輸入的值丟入類型與輸入相同的變數x
中
這只是個開始 – 模式是一個新的語言元素種類在 C# 中,而我們期望未來能新增更多到 C# 裡。
在 C# 7.0 我們以模式增強了兩個現有的語言結構:
- is 表達式現在可以有一個模式在右邊,而不是只是一個類型。
- 在
switch
陳述式中的 case 子句現在可以比對模式,而不只是常數值。
未來 C# 的版本我們可能會新增更多可以使用模式的地方。
使用模式的 is 表達式
此為使用常數模式和類型模式的 is
表達式範例:
public void PrintStars(object o)
{
if (o is null) return; // constant pattern "null"
if (!(o is int i)) return; // type pattern "int i"
WriteLine(new string('*', i));
}
正如你所看見的,模式變數 - 由模式引入的變數,和先前描述的 out 變數相似,它們可以在表達式中間被宣告,而且可以被使用在最接近的周邊作用範圍之內。同時像 out 變數,模式變數是可變動的。
Note:就像 out 變數,嚴格的範圍規定適用於 Preview 4。
模式與 Try-方法常常很好一起使用:
if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }
使用模式的 switch 陳述式
我們一般化 switch 陳述式讓:
- 您可以 switch 在任何類型(不只是原始物件類型)
- 模式可以被用在 case 子句
- case 子句可以有額外的條件在它們上面
這裡有一個簡單的例子:
switch(shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
default:
WriteLine("<unknown shape>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
有一些有關新擴充的 switch 陳述式需要注意事情:
- case 子句的順序是很重要的:就像 catch 子句,case 子句的範圍是可以相交的,第一個匹配到的子據將被執行。因此上面的例子中正方形的 case 要在長方形的 case 之前是重要的。此外,就像 catch 子句,編譯器將會幫助您放旗標在明顯不能匹配到的 case。在這之前您不能區別評估的順序,所以這不是一個重大改變的特性。
- default 子句總是最後被評估:在上面的例子中即使
null
case 在最後一個,可是它將會在 default 子句前檢查。這是為了現有 switch 語法的兼容性。然而,好的實作通常要你把 default 子句放在最後面。 - null 子句在最後面並不會無法達到:這是因為類型模式依循目前
is
表達式的例子,而並不匹配 null。這確保 null 值不會不小心被任何類型的模式給搶走;您必須更清楚要如何處理它們(或留它們給 default 子句)。
被一個 case …
引進的模式變數:標籤僅在相對應的 switch 範圍內。
Tuples
想要從一個方法中回傳超過一個的值還蠻常見的。目前有的選項還不是最佳的:
- out 參數:使用起來較為笨拙(即使有上述的改善),而且他們並不能用在非同步方法。
System.Tuple<…>
回傳類型:冗餘使用和請求一個 tuple 對象的分配。- 為每個方法訂製傳輸類型:對於一個類型會有很多程式碼的額外負荷,而目的只是要暫時聚集一些值。
- 透過
dynamic
回傳類型回傳匿名類型:很高的性能開銷,而且沒有靜態類型的檢查。
為了要做得更好,C# 7.0 新增了 tuple 類型與 tuple literal:
(string, string, string) LookupName(long id) // tuple return type
{
... // retrieve first, middle and last from data storage
return (first, middle, last); // tuple literal
}
方法現在有效率地回傳三個字串,包成一個元素在一個 tuple 的值。
呼叫方法的函式現在將會收到一個 tuple,而可以取得每一個獨立的元素:
var names = LookupName(id);
WriteLine($"found {names.Item1} {names.Item3}.");
Item 1
等等為 tuple 元素預設的名字,總是可以使用。但它們不是很敘述性的,所以您可以選擇新增一個更好的:
(string first, string middle, string last) LookupName(long id) // tuple elements have names
現在這個 tuple 可以有更多敘述性的名字可以使用:
var names = LookupName(id);
WriteLine($"found {names.first} {names.last}.");
您也可以直接指定元素的名字在 tuple literal:
return (first: first, middle: middle, last: last); // named tuple elements in a literal
一般來說可以给 tuple 類型分配一些彼此無關的名稱:只要各個元素是可分配的,tuple 類型就可以自由地轉換為其他的 tuple 類型。也有一些限制,特別是對 tuple literal,即常見的和警告錯誤,如不慎交換元素名稱的情況下,就會出現錯誤。
Note:這些限制還尚未被實作在 Preview 4中。
Tuple 為值類型,而它們的元素就只是公開的、可變動的欄位。它們有值相等,意思是說兩個 tuple 是相等的(而且有相同的 hash code)如果它們全部的元素都成對相等(而且有相同的 hash code)。
這讓 tuple 在多個回傳值之外的其他情況更有用。例如,如果您需要一個字典有多個鑰匙,使用 tuple 當您的鑰匙而一切都會進行得很順利。如果您需要一個清單有多個值在每個位置,使用 tuple 並搜尋清單等等,將會正確地運作。
Note:Tuple 依靠一組底層的類型,而這未包含在 Preview 4 中。為了讓功能可以使用,您可以從 NuGet 上取得它們:
- 在方案總管中右鍵點選專案,並選擇「管理方案的 NuGet 套件…」
- 選擇「瀏覽」頁面,勾選「包含搶鮮版」,並選擇「org」為「封裝來源」
- 搜尋「ValueTuple」並安裝它
解構
另一個取用 tuple 的方式就是解構它們。解構宣告的語法是拆解 tuple(或其他值)成它裡面的部分,並個別指派那些部分到新的變數:
(string first, string middle, string last) = LookupName(id1); // deconstructing declaration
WriteLine($"found {first} {last}.");
在解構宣告中您可以使用 var
在那些個別的變數上:
(var first, var middle, var last) = LookupName(id1); // var inside
或甚至可以簡化為只放一個 var
在括號外面:
var (first, middle, last) = LookupName(id1); // var outside
您也可以用解構賦值的方式解構到現有的變數:
(first, middle, last) = LookupName(id2); // deconstructing assignment
解構並不是只有 tuple 可以用。任何類型都可以被解構,只要它有一個(實體或擴展)像下面格式的解構函式方法:
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
out 參數構成的值從解構產生。
(為什麼使用 out 參數而不回傳 tuple呢?因為這樣您就可以有多個多載給不同數量的值)
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
(var myX, var myY) = GetPoint(); // calls Deconstruct(out myX, out myY);
讓建構函式與解構函式像這樣「對稱」將會是一個常見的模式。
如同 out 變數,我們計畫要在解構允許「萬用字元」,對於那些您不在意的東西:
(var myX, *) = GetPoint(); // I only care about myX
Note:仍然未確定萬用字元會不會在 C# 7.0 版本中。
區域函式
有時候一個輔助函式只在一個使用它的方法中有意義。您現在可以定義這樣的函式在其他函式裡面,作為一個區域函式:
public int Fibonacci(int x)
{
if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x));
return Fib(x).current;
(int current, int previous) Fib(int i)
{
if (i == 0) return (1, 0);
var (p, pp) = Fib(i - 1);
return (p + pp, p);
}
}
封閉區塊中的參數與區域變數可以在區域函式內使用,就像它們在匿名函式中一樣。
舉一個例子,迭代的方法實現通常需要一個非迭代的封裝方法(迭代器本身不啟動運行,直到 MoveNext
被呼叫)。區域函數非常適合這樣的情境:
public IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> filter)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (filter == null) throw new ArgumentNullException(nameof(filter));
return Iterator();
IEnumerable<T> Iterator()
{
foreach (var element in source)
{
if (filter(element)) { yield return element; }
}
}
}
如果 Iterator
是一個私用方法在 Filter
旁邊,它就能被其他成員不小心直接使用(沒有引數檢查)。此外,它也會需要拿所有與 Filter
一樣的引數,而不是只是有它們在作用範圍裡。
Note:在 Preview 4,區域函式必須宣告在它們被呼叫之前。這個限制將會鬆綁,只要它們讀取的區域變數一被確定指派,它們就可以被呼叫了。
Literal 改善
C# 7.0 允許 _
在數字 literal 當作數字分隔器:
var d = 123_456;
var x = 0xAB_CD_EF;
您可以把它們放在任何您想要的位置,來加強可讀性。它們對於值並沒有影響。
此外,C# 7.0 引入二進位 literal,這樣您就可以直接指定,而不需要知道十六進位的表示方式。
var b = 0b1010_1011_1100_1101_1110_1111;
參考回傳與區域
就像您可以在 C# 傳東西 by reference(用 ref
修飾詞),您現在可以回傳它們 by reference,而且儲存它們 by reference 在區域變數。
public ref int Find(int number, int[] numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] == number)
{
return ref numbers[i]; // return the storage location, not the value
}
}
throw new IndexOutOfRangeException($"{nameof(number)} not found");
}
int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // aliases 7's place in the array
place = 9; // replaces 7 with 9 in the array
WriteLine(array[4]); // prints 9
這在傳遞 placeholder 到大的資料結構很有用。例如,遊戲可能會把它的資料放在一個預先分配的 struct 陣列中(避免垃圾收集暫停)。方法現在可以直接回傳參考到這樣一個 struct,透過呼叫者可以讀取或修改它。
有一些限制來確保這是安全的:
- 您只可以回傳「安全回傳」的參考:一個是傳遞给你的引用,一個是指向對象中的引用。
- Ref locals 被初始化為某一儲存位置,而且不能改變到指向另一個。
一般化非同步回傳類型
到目前為止,非同步方法在 C# 中必須回傳 void
、Task
或 Task<T>
。C# 7.0 允許其他類型被這樣定義,讓它們可以從非同步方法回傳。
例如我們計畫要有一個 ValueTask<T>
的 struct 類型。它被建置來預防 Task<T>
物件的配置,萬一非同步操作的結果在等待時已經可以取得。對於很多非同步的情境,比如以涉及緩衝為例,這可以大大减少分配的數量,並使性能有顯著地提升。
有許多其他方式您可以想像自訂「任務式」類型是很有用的。正確地建立它們並不直觀,所以我們並不期待大多數的人推出他們自己的,但他們是有可能開始出現在 framework 與 API 中,而呼叫者就可以只回傳與 await 他們今天做 Tasks 的方式。
Note:一般化非同步回傳類型還未在 Preview 4 可以使用。
更多表達式來作為成員本體
Expression bodied 方法、屬性等等在 C# 6.0 大受歡迎,但我們並沒有允許它們在各類成員。C# 7.0 新增存取子、建構函式與完成項到可以有 expression bodies 的清單上:
class Person
{
private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
private int id = GetId();
public Person(string name) => names.TryAdd(id, name); // constructors
~Person() => names.TryRemove(id, out *); // destructors
public string Name
{
get => names[id]; // getters
set => names[id] = value; // setters
}
}
Note:這些額外的 expression bodied 成員的種類還不能在 Preview 4 中使用。
這是一個由社群貢獻的功能的例子,而不是 Microsoft C# 編譯器團隊。耶!開源!
Throw 運算式
在一個運算式中擲回例外狀況很容易:只要呼叫一個方法來幫您做這件事。但在 C# 7.0,我們直接允許 throw
為一個運算式在特定的地方:
class Person
{
public string Name { get; }
public Person(string name) => Name = name ?? throw new ArgumentNullException(name);
public string GetFirstName()
{
var parts = Name.Split(" ");
return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
}
public string GetLastName() => throw new NotImplementedException();
}
Note:擲回例外狀況尚未可以運作在 Preview 4。
本文翻譯自 What’s New in C# 7.0
若對以上技術及產品有任何問題,很樂意為您服務! 請洽:台灣微軟開發工具服務窗口 – MSDNTW@microsoft.com / 02-3725-3888 #4922