以下將是 C# 7.0 中所有計劃的語言特性的描述。隨著 Visual Studio “15” Preview 4 版本的發布,這些特性中的大部分將活躍起來。現在是時候來展示這些特性,你也告訴借此告訴我們你的想法!
C#7.0 增加了許多新功能,並專注於數據消費,簡化代碼和性能的改善。或許最大的特性就是元祖和模式匹配,元祖可以很容易地擁有多個返回結果,而模型匹配可以根據數據的“形”的不同來簡化代碼。我們希望,將它們結合起來,從而使你的代碼更加簡潔高效,也可以使你更加快樂並富有成效。
請點擊 Visual Studio 窗口頂部的反饋按鈕,告訴我們哪些是你不期待的特性或者你關於提升這些特性的思考。還有許多功能沒有在 Preview 4 版本中實現。接下來我會描述一些我們發布的最終版本裡將會起作用的特性,和一些一旦不起作用機即會刪除掉的特性。我也是支持對這些計劃作出改變,尤其是作為我們從你那兒得到反饋的結果。當最終版本發布時,這些特性中的一些將會改變或者刪除。
如果你好奇這些特性的設計過程,你可以在 Roslyn GitHub site 上找到很多設計筆記和討論。
希望 C#7.0 能帶給你快樂!
輸出變量
在當前的 C# 中,使用輸出參數並不像我們想的那樣方便。在你調用一個無輸出參數的方法之前,首先必須聲明一個變量並傳遞給它。如果你沒有初始化這些變量,你就無法使用 var 來聲明它們,除非先指定完整的類型:
public void PrintCoordinates(Point p)
{
int x, y; // have to "predeclare"
p.GetCoordinates(out x, out y);
WriteLine($"({x}, {y})");
}
在 C#7.0 中,我們正在增加輸出變量和聲明一個作為能夠被傳遞的輸出實參的變量的能力:
public void PrintCoordinates(Point p)
{
p.GetCoordinates(out int x, out int y);
WriteLine($"({x}, {y})");
}
注意,變量是在封閉塊的范圍內,所以後續也可以使用它們。大多數類型的聲明不建立自己的范圍,因此在他們中聲明的變量通常會被引入到封閉范圍。
Note:在 Preview 4 中,適用范圍規則更為嚴格:輸出變量的作用域是聲明它們的語句,因此直到下個版本發布時,上面的示例才會起作用。
由於輸出變量直接被聲明為實參傳遞給輸出形參,編譯器通常會告訴他們應該是的類型(除非有沖突過載),所以使用 var 來代替聲明它們的方式是比較好的:
p.GetCoordinates(out var x, out var y);
輸出參數的一種常見用法是Try模式,其中一個布爾返回值表示成功,輸出參數就會攜帶所獲的結果:
public void PrintStars(string s)
{
if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); }
else { WriteLine("Cloudy - no stars tonight!"); }
}
注意:這裡i只用在 if 語句來定義它,所以 Preview 4 可以將這個處理的很好。
我們計劃允許以 a* 為形式的“通配符”作為輸出參數,這會讓你忽略了你不關心參數:
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 形式的 Var 模式(x是一個標識符),它總是匹配的,並簡單地將輸入值以它原本的類型存入一個新變量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));
}
正如你們看到,模式變量(模式引入的變量)和早前描述的輸出變量比較類似,它們可以在表達式中間聲明,並在最近的范圍內使用。就像輸出變量一樣,模式變量是可變的。
注:就像輸出變量一樣,嚴格范圍規則適用於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 語句的范圍現在可以相交,第一個匹配上的會被選中。此外,就像 catch 語句一樣,編譯器通過去除明顯不會進入的 case 來幫助你。在此之前,你甚至不需要告訴判斷的順序,所以這並不是一個使用 case 語句的巨大的改變。
•默認的語句還是最後被判斷:盡管 null 的 case 語句在最後語句之前出現,它也會在默認語句被選中之前被測試。這是與現有 Switch 語義兼容的。然而,好的做法通常會將默認語句放到最後。
•Switch 不會到最後的 null 語句:這是因為當前 IS 表達式的例子具有類型匹配,不會匹配到 null。這保證了空值不會不小心被任何的類型模式匹配上的情況;你必須更明確如何處理它們(或放棄它而使用默認語句)。
通過一個 case 引入模式變量:標簽僅在相應的 Switch 范圍內。
元組
這是一個從方法中返回多個值的常見模式。目前可選用的選項並非是最佳的:
•輸出參數:使用起來比較笨拙(即使有上述的改進),他們在使用異步方法是不起作用的。
•System.Tuple<...> 返回類型:冗余使用和請求一個元組對象的分配。
•方法的定制傳輸類型:對於類型,具有大量的代碼開銷,其目的只是暫時將一些值組合起來。
•通過動態返回類型返回匿名類型:很高的性能開銷,沒有靜態類型檢查。
在這點要做到更好,C#7.0 增加的元組類型和元組文字:
(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}.");
你也可以直接在元組文字指定元素名稱:
return (first: first, middle: middle, last: last); // named tuple elements in a literal
一般可以給元組類型分配一些彼此無關的名稱:只要各個元素是可分配的,元組類型就可以自如地轉換為其他的元組類型。也有一些限制,特別是對元組文字,即常見的和告警錯誤,如不慎交換元素名稱的情況下,就會出現錯誤。
Note:這些限制尚未在 Preview 4 中實現。
元組是值類型的,它們的元素是公開的,可變的。他們有值相等,如果所有的元素都是成對相等的(並且具有相同的哈希值),那麼這兩個元組也是相等的(並且具有相同的哈希值)。
這使得在需要返回多個值的情況下,元組會非常有用。舉例來說,如果你需要多個 key 值的字典,使用元組作為你的 key 值,一切會非常順利。如果你需要在每個位置都具有多個值的列表,使用元組進行列表搜索,會工作的很好。
Note:元組依賴於一組基本類型,卻不包括在 Preview 4 中。為了使該特性工作,你可以通過 NuGet 獲取它們:
•右鍵單擊 Solution Explorer 中的項目,然後選擇“管理的NuGet包......”
•選擇“Browse”選項卡,選中“Include prerelease”,選擇“nuget.org”作為“Package source”
•搜索“System.ValueTuple”並安裝它。
解構
消耗元組的另一種方法是將解構它們。一個解構聲明是一個將元組(或其他值)分割成部分並單獨分配到新變量的語法:
(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
解構不只是應用於元組。任何的類型都可以被解構,只要它具有(實例或擴展)的解構方法:
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
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);
}
}
閉合范圍內的參數和局部變量在局部函數的內部是可用的,就如同它們在 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,局部函數在調用之前,必須被聲明。這個限制將會被松開,以便使得局部函數從定義分配中讀取時,能夠被調用。
文字改進
C#7.0 允許 _ 出現,作為數字分隔號:
var d = 123_456;
var x = 0xAB_CD_EF;
你可以將 _ 放入任意的數字之間,以提高可讀性,它們對值沒有影響。
此外,C#7.0 引入了二進制文字,這樣你就可以指定二進制模式而不用去了解十六進制。
var b = 0b1010_1011_1100_1101_1110_1111;
引用返回和局部引用
就像在 C# 中通過引用來傳遞參數(使用引用修改器),你現在也可以通過引用來返回參數,同樣也可以以局部變量的方式存儲參數。
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
這是繞過占位符進入大數據結構的好方法。例如,一個游戲也許會將它的數據保存在大型預分配的陣列結構中(為了避免垃圾回收機制暫停)。方法可以將直接引用返回成一個結構,通過它的調用者可以讀取和修改它。
也有一些限制,以確保安全:
•你只能返回“安全返回”的引用:一個是傳遞給你的引用,一個是指向對象中的引用。
•本地引用會被初始化成一個本地存儲,並且不能指向另一個存儲。
異步返回類型
到現在為止,C# 的異步方法必須返回 void,Task 或 Task<T>。C#7.0 允許其它類型以這種能從一個方法中返回的方式被定義,因為它們可以以異步方法被返回的方式來定義其它類型。
例如我們計劃建立一個 ValueTask<T> 結構類型的數據。建立它是為了防止異步運行的結果在等待時已可用的情境下,對 Task<T> 進行分配。對於許多實例中設計緩沖的異步場景,這可以大大減少分配的數量並顯著地提升性能。
Note:異步返回類型尚未在 Preview 4 中提供。
更多的 expression bodied 成員:
expression bodied 的方法和屬性是對 C# 6.0 的巨大提升。C# 7.0 為 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
}
}
Note:這些額外增加的 expression bodied 的成員尚未在 Preview 4 中提供。
這是社區共享的示例,而不是 Microsoft C# 編譯團隊提供的,還是開源的!
Throw 表達式
在表達式中間拋出一個異常是很容易的:只需為自己的代碼調用一個方法!但在 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();
}
Note:Throw 表達式尚未在Preview 4中提供。
文章來源:Mads Torgersen - MSFT
原文鏈接:https://blogs.msdn.microsoft.com/dotnet/2016/08/24/whats-new-in-csharp-7-0/