C# 7.0 語言新特性
下面是關於在C#7.0語言中計劃功能的說明。其中大部分功能在Visual Studio “15” Preview 4中能執行。現在是最好的試用時期,請記錄下你們的想法。
C#7.0語言增加了許多的新功能,促使專注於資料消費,簡化程式碼和效能。
或許最大的特徵是元組(tuples) ,使得它易於有多個結果,並從而簡化程式碼是以對資料的形狀為條件的模式匹配。但也有許多其他的一些功能希望將它們結合起來,讓程式碼執行更高效,更明確,從而獲得更多的創造性。如果有哪些執行不是你想要的或者有想改進的功能,在Visual Studio的視窗頂部使用“send feedback”功能將結果反饋給我們。在我所描述的許多功能在Preview 4還沒有辦法充分執行,根據使用者的反饋結果,我們將在釋出最終版是增加些新的功能。而必須要指出的是,現有計劃中的一些功能在最終版也可能會有所改變或取消。
如果你對這個功能設定感興趣並想學習它,在Roslyn GitHub site上可以找到許多的設計說明和相關討論。
輸出(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變數通常在宣告中被引入到封閉範圍。
注:在Preview 4中,適用範圍規則更為嚴格:out變數的作用域為它們在宣告的說法。因此,上面的例子不會在後續的版本中使用。
由於out變數直接宣告作為引數傳遞給out引數,編譯器通常可以告知型別(除非有衝突的過載)。所以這是很好用VAR,而不是一個型別來宣告它們:
p.GetCoordinates(out var x, out var y);
out引數的一個常見的用途是Try...模式,其中out引數一個boolean return表示成功,out引數進行得到的結果:
public void PrintStars(string s) { if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); } else { WriteLine("Cloudy - no stars tonight!"); } }
注:Preview 4處理的比較好的地方在於只是用if語句定義它。
計劃允許“wildcards”作為out引數以及在*的形式,忽視不重要的out引數:
p.GetCoordinates(out int x, out *); // I only care about x
注:wildcards能否把它變成C#7.0還是個未知數。
模式匹配
C# 7.0 引入了模式的概念,抽象地說,這是一種語法成分可以用來測試一個值是否有一個一定的“形”以及在它起作用時從值裡面獲取到的額外資訊。
下面是 C# 7.0 中關於模式的例子:
-
c 的常量模式(c 是C#中的一個常量表示式),用於測試輸入的引數是否和 c 相等
-
T x 的型別模式(T 是一個型別,x 是一個識別符號),用於測試輸入的引數是否有型別 T,如果有,提取輸入引數的值到一個 T 型別的新 x 變數中。
-
var x 變數模式(x 是一個識別符號),通常會匹配並簡單地將輸入引數的值放進一個新變數 x 中
這是個開始,模式是一種新的 C# 語言元素,而且我們將來可以把它們更多地增加到 C# 中。
在 C# 7.0 中,我們正在使用模式以增強兩種已存在的語言結構:
-
is 表示式現在在右邊可以有一個模式,而不只是一個型別
-
case 子句在 switch 語句中現在可以通過模式匹配,而不僅僅是常量值
在將來的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 變數那樣,模式變數是易變的,
注: 就像 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 子句不再是必然不相交的,第一個子句匹配的將被選擇。因此這裡重要的是上面程式碼中 square case 比 rectangle case 來得要早。也是和 catch 子句一樣,編譯器會通過標記明顯不能到達的情況來幫助你。在這之前,你永遠無法知道評價的順序,所以這不是一個重大改變的特性。
- 預設子句總是最後被評價:即使上面程式碼中 null 子句是最後才來,它會在預設子句被選擇前被檢查。這是為了與現有 switch 語義相相容。然而,好的做法通常會讓你把預設子句放到最後。
- null 子句在最後不可到達:這是因為型別模式遵循當前的 is 表示式的例子並且不會匹配空值。這保證了空值不會偶然被任何的型別模式捎來第一搶購。你必須更明確如何處理它們(或為預設子句留下他們)。
通過 case ...: 標籤引入的模式變數僅存在於相對應的 switch 部分的範圍內。
元組
這是常見的希望從一個方法返回多個值的做法。目前可用的選項不是最佳的:
- Out 引數。使用笨拙(即便有上面描述到的提升),它們不使用非同步的方法執行。
- System.Tuple<...> 返回型別。使用累贅並且需要一個元組物件的分配。
- 為每個方法定製傳輸型別:大量的程式碼為了型別開銷的目的僅是臨時收集一些值
- 匿名型別通過返回一個 dynamic 返回型別。高效能開銷並且沒有靜態型別檢查。
為了在這方面做得更好,C# 新增了tuple types 和 tuple literals:
(string, string, string) LookupName(long id) // tuple return type { ... // retrieve first, middle and last from data storage return (first, middle, last); // tuple literal }
這個方法目前有效地返回三個字串,將其作為元素在元組型別裡包裹起來。
方法的呼叫者將會接受到一個元組,並且可以逐一訪問元素。
var names = LookupName(id); WriteLine($"found {names.Item1} {names.Item3}.");
Item1 等等,是元組元素的預設名字,並能夠經常被使用。但它們不是太好描述的,因此你可以選擇性地新增更好的一個。
(string first, string middle, string last) LookupName(long id) // tuple elements have names
現在元組的接受者擁有更多的可描述的名字用於執行:
var names = LookupName(id); WriteLine($"found {names.first} {names.last}.");
你也可以在 tuple literals 中直接指定名字:
return (first: first, middle: middle, last: last); // named tuple elements in a literal
通常來說,你可以互相分配元組型別無關的名字,只要獨立的元素是可以被分配的,元組型別會自如 轉換成其他元組型別。特別是對於 tuple literals ,存在一些限制,這會警告或提示在常見的錯誤的情況下提示,例如偶然交換元素的名字。
注意:這些限制還沒在 Preview 4 中實現
元組是值型別,而且他們的元素只是公開、易變的域。他們的值相等,代表這兩個元組是相等的(都有相同的哈斯碼)如果它們的元素都結對匹配(都有相同的哈斯碼)。
這使得元組對於在多種返回值下的很多情況十分有用。舉例來說,如果你需要一個有多種鍵的詞典,使用元組作為你的鍵,然後一切東西就會如常工作。如果你需要在每個位置有一個有多種值的列表,使用元組,查詢列表等等,程式會正常執行。
注意:元組依賴一系列底層型別,它們在 Preview 4 中不被引入。為了將來的工作,你可以通過 NuGget 輕易獲取它們: 在 Solution Explorer 中右鍵點選專案,並選擇“Manage NuGet Packages…” 選擇“Browse”選項卡,檢查“Include prerelease” 並且選擇“nuget.org”作為“Package source” 搜尋“System.ValueTuple”並安裝它
解構
另一種消除元組(tuple)的方法是解構元組。通過一個解構宣告語法,把一個元組(或者其他的值)拆分為幾部分,並且重新定義為新的變數。
(string first, string middle, string last) = LookupName(id1); // deconstructing decla rationWriteLine($"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
不僅僅元組可以被解構,任何型別都可以被解構,只要有一個對應的(實體或者擴充套件)解構方法:
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
輸出引數由解構之後的結果值構成。
(為什麼採用資料引數代替返回一個元組?這樣,你可以過載多個不同的數值)
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);
這將成為一種常見模式,包含解構函式和“對稱”解析:
針對輸出變數,我們計劃在解構中允許使用“萬用字元”:
(var myX, *) = GetPoint(); // I only care about myX
注:仍然還沒有確定是否將萬用字元引入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); } }
引數和閉合區間區域性變數可用在區域性函式內,類似lambda表示式。
舉一個例子,方法實現迭代器通常需要嚴格檢查呼叫時非迭代器封裝方法。(迭代器本身沒有執行,只到呼叫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; } } } }
如果迭代器是一個私有方法的下一個過濾器,它將有可能被其他成員不小心使用(沒有引數檢查)。此外,作為過濾器,它將需要採取所有的相同的引數,而不是指定域內的引數。
注:在Preview 4版本中,本地函式必須在它們被呼叫之前宣告。這個限制將被放鬆,能呼叫讀取直接賦值的區域性變數。
Literal 改進
C# 7.0 允許使用“_”作為數字分隔符在數字literals中:
var d = 123_456; var x = 0xAB_CD_EF;
你可以把它放在你想要的位置,提升可讀性。這樣對數值沒有任何影響。
此外,C# 7.0也介紹了二進位制literals,這樣你可以直接指定二進位制模式而不必知道十六進位制符號。
var b = 0b1010_1011_1100_1101_1110_1111;
Ref 返回和本地
就像你可以通過reference(用ref修飾符)在C#中傳遞東西,您現在可以通過reference return 他們,並通過 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
這對繞過佔位符成為大資料結構是非常有用的。舉例來說,一個遊戲可能會在一個大的預分配陣列結構中儲存其資料(為避免垃圾收集暫停)。Methods 可以直接返回一個 reference 到這樣一個結構,且通過呼叫者可以讀取和修改它。
這裡有一些限制,以確保這是安全的:
-
你可以只返回 refs 那些是 “安全返回(safe to return)”的:那些被傳遞給你的,和那些點到物件的欄位。
-
Ref locals被初始化為某一儲存位置,並且不能突變到指向另一個。
廣義非同步返回型別
截至目前為止,在C#呼叫非同步方法必須要返回void,Task或Task<T>。C#7.0允許以這樣的方式來定義其它型別,從而使它們可以從非同步方法返回。
例如,我們計劃有一個ValueTask<T>結構型別。它是建立在預防Task<T> 物件的分配時,非同步操作的結果是已在可等候的時間的情況下。對於很多非同步場景,比如以涉及緩衝為例, 這可以大大減少分配的數量,並使效能有顯著提升。
這裡有許多其他的方法可以讓您想象自定義“task-like”型別是有用的。它不會是簡單的正確建立,所以我們不要指望大多數人推出自己的,但他們很可能將會開始在框架和API展現出來,然後呼叫方可以返回並await他們今天做任務(Tasks)的方式。
注:廣義非同步返回型別尚未應用在預覽4。
更多 Expression-bodied 方法
Expression-bodied 方法、屬性等都是C# 6.0的重大突破,但並不允許他們在各種各樣的member中使用,C#7.0新增了訪問器、建構函式和終結器等,使更多member可以使用Expression-bodied 方法:
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 } }
注:這些額外的Expression-bodied 方法還沒有工作在預覽4。
這是一個由社群貢獻的特徵,而非微軟C#團隊。並且,開源!
在表示式的中間丟擲一個異常是很容易的,只需要為你自己呼叫一個方法,但在C#7.0中我們允許在一些地方直接丟擲表示式:
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(); }
注:丟擲表示式還未在預覽4工作。
本文地址:http://www.oschina.net/translate/whats-new-in-csharp-7-0
原文地址:https://blogs.msdn.microsoft.com/dotnet/2016/08/24/whats-new-in-csharp-7-0/
相關文章
- PHP7.0 的新特性PHP
- C++11新特性(一):語言特性C++
- C++11新特性(三):語言特性C++
- C++11新特性(二):語言特性C++
- 詳解C#7.0新特性C#
- 值得推薦的C#不同版本語言特性C#
- Redis 7.0 新功能新特性總覽Redis
- C# 語言歷史版本特性(C# 1.0到C# 8.0彙總)C#
- C#語言歷史版本特性(C# 1.0到C# 8.0彙總)C#
- C#7.0新特性(VS2017可用)C#
- Java從8到21的語言新特性Java
- C++2.0——語言新特性之Variadic TemplatesC++
- C# 9.0新特性C#
- C# 10的新特性C#
- Java8 新特性 —— 函數語言程式設計Java函數程式設計
- C#語言————第二章 C#語言快速熱身C#
- C# 11 都有哪些新特性?C#
- C# 9 新特性——init only setterC#
- JDK 8 新特性之函數語言程式設計 → Stream APIJDK函數程式設計API
- Python 語言特性:編譯+解釋、動態型別語言、動態語言Python編譯型別
- Android 7.0新特性——桌面長按圖示出現快捷方式Android
- 快速瞭解C# 8.0中“可空引用型別(Nullable reference type)”語言特性C#型別Null
- C# 9 新特性 —— 增強的 foreachC#
- C# LINQ (語言整合查詢)C#
- C#語言函式遞迴C#函式遞迴
- C# 9.0 新特性之模式匹配簡化C#模式
- C# 9 新特性 —— 增強的模式匹配C#模式
- 2021 程式語言排行榜出爐!C#年度語言獎C#
- Rust入門系列之語言特性 - 1Rust
- Go 語言的 4 個特性改動Go
- C#特性C#
- C# 9.0 新特性之 Lambda 棄元引數C#
- 你所不知道的 C# 10新特性C#
- Chrome 71 新特性[雙語+視訊]Chrome
- C語言的角落——這些C語言不常用的特性你知道嗎?C語言
- C++20語言核心特性的變化C++
- ES6語言特性的總結(3)
- 使用 C# 9.0 新語法提升 if 語句美感C#
- 說說 C# 9 新特性的實際運用C#