C#7.0 於 2017年3月 隨 .NET 4.7 和 VS2017 發布。
一. out 變量(out variables)
以前我們使用out變量必須在使用前進行聲明,C# 7.0 給我們提供了一種更簡潔的語法 “使用時進行內聯聲明” 。如下所示:
var input = ReadLine(); if (int.TryParse(input, out var result)) { WriteLine("您輸入的數字是:{0}", result); } else { WriteLine("無法解析輸入..."); }
上面代碼編譯后:
int num; string s = Console.ReadLine(); if (int.TryParse(s, out num)) { Console.WriteLine("您輸入的數字是:{0}", num); } else { Console.WriteLine("無法解析輸入..."); }
原理解析:所謂的 “內聯聲明” 編譯后就是以前的原始寫法,只是現在由編譯器來完成。
備注:在進行內聯聲明時,即可直接寫明變量的類型也可以寫隱式類型,因為out關鍵字修飾的一定是局部變量。
二. 元組(Tuples)
元組(Tuple)在 .Net 4.0 的時候就有了,但元組也有些缺點,如:
1)Tuple 會影響代碼的可讀性,因為它的屬性名都是:Item1,Item2.. 。
2)Tuple 還不夠輕量級,因為它是引用類型(Class)。
備注:上述所指 Tuple 還不夠輕量級,是從某種意義上來說的或者是一種假設,即假設分配操作非常的多。
C# 7 中的元組(ValueTuple)解決了上述兩個缺點:
1)ValueTuple 支持語義上的字段命名。
2)ValueTuple 是值類型(Struct)。
1. 如何創建一個元組?
var tuple = (1, 2);// 使用語法糖創建元組 var tuple2 = ValueTuple.Create(1, 2); // 使用靜態方法【Create】創建元組 var tuple3 = new ValueTuple<int, int>(1, 2); // 使用 new 運算符創建元組 WriteLine($"first:{tuple.Item1}, second:{tuple.Item2}, 上面三種方式都是等價的。");
原理解析:上面三種方式最終都是使用 new 運算符來創建實例。
2. 如何創建給字段命名的元組?
// 左邊指定字段名稱 (int one, int two) tuple = (1, 2); WriteLine($"first:{tuple.one}, second:{tuple.two}"); // 右邊指定字段名稱 var tuple2 = (one: 1, two: 2); WriteLine($"first:{tuple2.one}, second:{tuple2.two}"); // 左右兩邊同時指定字段名稱 (int one, int two) tuple3 = (first: 1, second: 2); /* 此處會有警告:由於目標類型(xx)已指定了其它名稱,因為忽略元組名稱xxx */ WriteLine($"first:{tuple3.one}, second:{tuple3.two}");
注:左右兩邊同時指定字段名稱,會使用左邊的字段名稱覆蓋右邊的字段名稱(一一對應)。
原理解析:上述給字段命名的元組在編譯后其字段名稱還是:Item1, Item2...,即:“命名”只是語義上的命名。
3. 什么是解構?(不推薦)
解構顧名思義就是將整體分解成部分。
4. 解構元組,如下所示:
var (one, two) = GetTuple(); WriteLine($"first:{one}, second:{two}"); static (int, int) GetTuple() { return (1, 2); }
原理解析:解構元組就是將元組中的字段值賦值給聲明的局部變量(編譯后可查看)。
備注:在解構時“=”左邊能提取變量的數據類型(如上所示),元組中字段類型相同時即可提取具體類型也可以是隱式類型,但元組中字段類型
不相同時只能提取隱式類型。
5. 解構可以應用於 .Net 的任意類型,但需要編寫 Deconstruct 方法成員(實例或擴展)。
如下所示:
public class Student { public Student(string name, int age) { Name = name; Age = age; } public string Name { get; set; } public int Age { get; set; } public void Deconstruct(out string name, out int age) { name = Name; age = Age; } }
使用方式如下:
var(Name, Age) = new Student("Mike", 30);
WriteLine($"name:{Name}, age:{Age}");
原理解析:編譯后就是由其實例調用 Deconstruct 方法,然后給局部變量賦值。
Deconstruct 方法簽名:
// 實例簽名 public void Deconstruct(out type variable1, out type variable2...) // 擴展簽名 public static void Deconstruct(this type instance, out type variable1, out type variable2...)
總結:
- 元組的原理是利用了成員類型的嵌套或者是說成員類型的遞歸。
- 編譯器很牛B才能提供如此優美的語法。
使用 ValueTuple 則需要導入: Install - Package System.ValueTuple
三. 模式匹配(Pattern matching)
1. is 表達式(is expressions)
如:
static int GetSum(IEnumerable<object> values) { var sum = 0; if (values == null) return sum; foreach (var item in values) { if (item is short) // C# 7 之前的 is expressions { sum += (short)item; } else if (item is int val) // C# 7 的 is expressions { sum += val; } else if (item is string str && int.TryParse(str, out var result)) // is expressions 和 out variables 結合使用 { sum += result; } else if (item is IEnumerable<object> subList) { sum += GetSum(subList); } } return sum; }
使用方法:
條件控制語句(obj is type variable) { // Processing... }
原理解析:此 is 非彼 is ,這個擴展的 is 其實是 as 和 if 的組合。即它先進行 as 轉換再進行 if 判斷,判斷其結果是否為 null,不等於 null 則執行
語句塊邏輯,反之不行。由上可知其實C# 7之前我們也可實現類似的功能,只是寫法上比較繁瑣。
2. switch語句更新(switch statement updates)
如:
static int GetSum(IEnumerable<object> values) { var sum = 0; if (values == null) return 0; foreach (var item in values) { switch (item) { case 0: // 常量模式匹配 break; case short sval: // 類型模式匹配 sum += sval; break; case int ival: sum += ival; break; case string str when int.TryParse(str, out var result): // 類型模式匹配 + 條件表達式 sum += result; break; case IEnumerable<object> subList when subList.Any(): sum += GetSum(subList); break; default: throw new InvalidOperationException("未知的類型"); } } return sum; }
使用方法:
switch (item) { case type variable1: // processing... break; case type variable2 when predicate: // processing... break; default: // processing... break; }
原理解析:此 switch 非彼 switch,編譯后你會發現擴展的 switch 就是 as 、if 、goto 語句的組合體。同 is expressions 一樣,以前我們也能實
現只是寫法比較繁瑣並且可讀性不強。
總結:模式匹配語法是想讓我們在簡單的情況下實現類似與多態一樣的動態調用,即在運行時確定成員類型和調用具體的實現。
四. 局部引用和引用返回 (Ref locals and returns)
我們知道 C# 的 ref 和 out 關鍵字是對值傳遞的一個補充,是為了防止值類型大對象在Copy過程中損失更多的性能。現在在C# 7中 ref 關鍵字得
到了加強,它不僅可以獲取值類型的引用而且還可以獲取某個變量(引用類型)的局部引用。如:
static ref int GetLocalRef(int[,] arr, Func<int, bool> func) { for (int i = 0; i < arr.GetLength(0); i++) { for (int j = 0; j < arr.GetLength(1); j++) { if (func(arr[i, j])) { return ref arr[i, j]; } } } throw new InvalidOperationException("Not found"); }
使用方法:
int[,] arr = { { 10, 15 }, { 20, 25 } }; ref var num = ref GetLocalRef(arr, c => c == 20); num = 600; Console.WriteLine(arr[1, 0]);
Print results:

使用方法:
1. 方法的返回值必須是引用返回:
a) 聲明方法簽名時必須在返回類型前加上 ref 修飾。
b) 在每個 return 關鍵字后也要加上 ref 修飾,以表明是返回引用。
2. 分配引用(即賦值),必須在聲明局部變量前加上 ref 修飾,以及在方法返回引用前加上 ref 修飾。
注:C# 開發的是托管代碼,所以一般不希望程序員去操作指針。並由上述可知在使用過程中需要大量的使用 ref 來標明這是引用變量(編譯后其
實沒那么多),當然這也是為了提高代碼的可讀性。
總結:雖然 C# 7 中提供了局部引用和引用返回,但為了防止濫用所以也有諸多約束,如:
1. 你不能將一個值分配給 ref 變量,如:
ref int num = 10; // error:無法使用值初始化按引用變量
2. 你不能返回一個生存期不超過方法作用域的變量引用,如:
public ref int GetLocalRef(int num) => ref num; // error: 無法按引用返回參數,因為它不是 ref 或 out 參數
3. ref 不能修飾 “屬性” 和 “索引器”。
var list = new List<int>(); ref var n = ref list.Count; // error: 屬性或索引器不能作為 out 或 ref 參數傳遞
原理解析:非常簡單就是指針傳遞,並且個人覺得此語法的使用場景非常有限,都是用來處理大對象的,目的是減少GC提高性能。
五. 局部函數(Local functions)
C# 7 中的一個功能“局部函數”,如下所示:
static IEnumerable<char> GetCharList(string str) { if (IsNullOrWhiteSpace(str)) throw new ArgumentNullException(nameof(str)); return GetList(); IEnumerable<char> GetList() { for (int i = 0; i < str.Length; i++) { yield return str[i]; } } }
使用方法:
[數據類型,void] 方法名([參數]) { // Method body;[] 里面都是可選項 }
原理解析:局部函數雖然是在其他函數內部聲明,但它編譯后就是一個被 internal 修飾的靜態函數,它是屬於類,至於它為什么能夠使用上級函
數中的局部變量和參數呢?那是因為編譯器會根據其使用的成員生成一個新類型(Class/Struct)然后將其傳入函數中。由上可知則局部函數的聲
明跟位置無關,並可無限嵌套。
總結:個人覺得局部函數是對 C# 異常機制在語義上的一次補充(如上例),以及為代碼提供清晰的結構而設置的語法。但局部函數也有其缺點,
就是局部函數中的代碼無法復用(反射除外)。
六. 更多的表達式體成員(More expression-bodied members)
C# 6 的時候就支持表達式體成員,但當時只支持“函數成員”和“只讀屬性”,這一特性在C# 7中得到了擴展,它能支持更多的成員:構造函數、析構函數、帶 get,set 訪問器的屬性、以及索引器。如下所示:
public class Student { private string _name; // Expression-bodied 構造函數 public Student(string name) => _name = name; // Expression-bodied 析構函數 ~Student() => Console.WriteLine("Finalized!"); // Expression-bodied 屬性訪問器
public string Name { get => _name; set => _name = value ?? "Mike"; } // Expression-bodied 索引器
public string this[string name] => Convert.ToBase64String(Encoding.UTF8.GetBytes(name)); }
備注:索引器其實在C# 6中就得到了支持,但其它三種在C# 6中未得到支持。
七. Throw 表達式(Throw expressions)
異常機制是C#的重要組成部分,但在以前並不是所有語句都可以拋出異常的,如:條件表達式(? :)、null合並運算符(??)、一些Lambda表達式。而使用 C# 7 您可在任意地方拋出異常。如:
public class Student { private string _name = GetName() ?? throw new ArgumentNullException(nameof(GetName)); private int _age; public int Age { get => _age; set => _age = value <= 0 || value >= 130 ? throw new ArgumentException("參數不合法") : value; } static string GetName() => null; }
八. 擴展異步返回類型(Generalized async return types)
在之前我們想用“async”、“await”就必須使用Task作為返回值(void特殊情況忽略),但Task是一個引用類型(class),這樣在非常簡單的任務中會造成浪費(內存和gc)。
以前異步的返回類型必須是:Task、Task<T>、void,現在 C# 7 中新增了一種類型:ValueTask<T>,如下所示:
public async ValueTask<int> Func() { await Task.Delay(3000); return 100; }
總結:ValueTask<T> 與 ValueTuple 非常相似,所以就不列舉: ValueTask<T> 與 Task 之間的異同了,但它們都是為了優化特定場景性能而
新增的類型。
使用 ValueTask<T> 則需要導入: Install - Package System.Threading.Tasks.Extensions
九. 數字文本語法的改進(Numeric literal syntax improvements)
C# 7 還包含兩個新特性:二進制文字、數字分隔符,如下所示:
var one = 0b0001; var sixteen = 0b0001_0000; long salary = 1000_000_000; decimal pi = 3.141_592_653_589m;
注:二進制文本是以0b(零b)開頭,字母不區分大小寫;數字分隔符只有三個地方不能寫:開頭,結尾,小數點前后。
總結:二進制文本,數字分隔符 可使常量值更具可讀性。
