一覽 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# 中必須回傳 voidTaskTask<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

 


VS

若對以上技術及產品有任何問題,很樂意為您服務! 請洽:台灣微軟開發工具服務窗口 – MSDNTW@microsoft.com / 02-3725-3888 #4922