Ask Learn
Preview
Please sign in to use this experience.
Sign inThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
以下要帶來的是所有在 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!
目前在 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 x
表示(x
為識別項<code),這種情況總是會匹配成功,所以就只是把輸入的值丟入類型與輸入相同的變數 x
中這只是個開始 – 模式是一個新的語言元素種類在 C# 中,而我們期望未來能新增更多到 C# 裡。
在 C# 7.0 我們以模式增強了兩個現有的語言結構:
switch
陳述式中的 case 子句現在可以比對模式,而不只是常數值。未來 C# 的版本我們可能會新增更多可以使用模式的地方。
此為使用常數模式和類型模式的 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(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 陳述式需要注意事情:
null
case 在最後一個,可是它將會在 default 子句前檢查。這是為了現有 switch 語法的兼容性。然而,好的實作通常要你把 default 子句放在最後面。is
表達式的例子,而並不匹配 null。這確保 null 值不會不小心被任何類型的模式給搶走;您必須更清楚要如何處理它們(或留它們給 default 子句)。被一個 case …
引進的模式變數:標籤僅在相對應的 switch 範圍內。
想要從一個方法中回傳超過一個的值還蠻常見的。目前有的選項還不是最佳的:
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,區域函式必須宣告在它們被呼叫之前。這個限制將會鬆綁,只要它們讀取的區域變數一被確定指派,它們就可以被呼叫了。
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,透過呼叫者可以讀取或修改它。
有一些限制來確保這是安全的:
到目前為止,非同步方法在 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# 編譯器團隊。耶!開源!
在一個運算式中擲回例外狀況很容易:只要呼叫一個方法來幫您做這件事。但在 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
Please sign in to use this experience.
Sign in