Ask Learn
Preview
Ask Learn is an AI assistant that can answer questions, clarify concepts, and define terms using trusted Microsoft documentation.
Please sign in to use Ask Learn.
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 的新語法。這也是在 2017/03/07 發表的 Visual Studio 2017 中眾多新功能之一。
在 C# 7.0 新增了許多支援的語法,重點擺在改善效能、精簡程式碼、以及資料取用幾個部分。其中最主要的功能之一是 Tuples, 能讓你更容易的一次傳回多筆結果,另外 Pattern Match 新語法則簡化了撰寫針對特定資料型態與條件的程式碼。除此之外,C# 7.0 也包含了其他重要的各種新語法支援。希望所有的這些改變都能讓你更愉快的寫出有效率,簡潔的程式碼,同時也更有生產力。
如果你很好奇我們如何導引出這些功能的設計過程,可以查閱 C# Language design GitHub 網站,在那邊可以找到設計說明文件,設計提案,與大量的討論內容。
如果你覺得這篇文章內容很熟悉,也許是你曾經看過去年八月份 (2016/08) 發表過的版本。在 C# 7.0 最終定案的版本中有少數的異動,這些
異動都來自先前版本的眾多優良的回饋意見。
希望你喜歡 C# 7.0, 盡情享受它, Happy Hacking !!
Mads Torgersen, C# Language PM
譯註:
為了更清楚的表達這篇文章的內容,翻譯時我採用意譯,而非逐句翻譯。我也會適時補充字句,讓文章要表達的意義更清楚完整。
太多專有名詞,翻成中文反而對閱讀沒有幫助,因此這部分我保留原文,但是我會在譯註的部分額外補充說明。
期望這樣能更清楚的讓讀者了解內容。
在先前版本的 C# 中,out
參數的使用並不如我們期望的那麼的流暢。呼叫帶有 out
參數的 method 之前,你必須先宣告變數
並且將它當作 out
的參數傳遞才行。通常你不會 (也不需要) 先初始化這變數 (變數的內容會在被呼叫的 method 內覆寫),同時你也不能使用 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
變數都會直接被宣告為傳遞的參數,編譯器通常能直接判定參數的型別為何 (除非 method 包含數個互相衝突
的 overloads 而無法判定),因此可以直接使用 var
的方式來宣告它:
p.GetCoordinates(out var x, out var y);
一般來說,我們常常在 Try...
這類的使用模式中用到 out
參數,它會傳回 true
或是 false
來代表執行成功與否,同時藉著 out
參數來傳回成功執行後的結果:
public void PrintStars(string s)
{
if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); }
else { WriteLine("Cloudy - no stars tonight!"); }
}
如果你不在意某個 out
參數的傳回結果,可以使用 _
代表忽略它:
p.GetCoordinates(out var x, out _); // I only care about x
C# 7.0 開始引入了 patterns (模式) 的概念。抽象的來說,他是可以判定資料是否具備一定 "形狀"(Shape) 的語法元素,並從該數值之中提取需要的資訊。
譯註:
Shape, 代表資料的 "形狀", 精確的來說包含資料本身型別包含哪些成員? 這些成員的數值是否落在預期的範圍?
patterns 可以讓判斷資料 "形狀" 的程式碼更為簡潔明確。
舉例來說,C# 7.0 支援的 patterns 有這幾種:
c
表示,c 是 C# 的常數表達式), 測試輸入的數值是否與 c 相等。T x
表示,T 代表型別,而 x 是識別名稱), 測試輸入的數值是否屬於類別 T? 如果是的話就把輸入的數值放到類型為 T 的變數 x 中。var x
表示, x 是識別名稱), 這種模式下永遠會匹配成功,此時 x 的型別與輸入的數值相同,這模式下只是簡單的把輸入的數值放到 x 之中。這些只是計畫中的第一步 - pattern (模式) 是 C# 新型態的語法元素,我們期望未來能繼續新增更多的功能。
在 C# 7.0 我們用 pattern 來增強兩種既有的語法結構:
在未來的 C# 我們會增加更多適用 pattern 的語法。
來看看使用 constant patterns 與 type patterns 的 is expression 使用範例:
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));
}
如所見,pattern 變數 - 由 pattern 引入的變數,跟前面介紹的 out
變數非常相似,你可以宣告在表達式之中,而且可以直接就近在同可是範圍內直接使用他。
跟 out
變數很相似的地方是,模式變數是可變動的,我們常將 out
變數與 pattern 變數,統稱為 expression 變數。
Patterns 常與 Try...
method 一起使用:
if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }
在 C# 7.0,我們也擴大了 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 陳述式新增的擴充功能:
case Rectangle s when (s.Length == s.Height):
) 應該要排在代表長方形的 case 子句 (case Rectangle r:
) 前面,結果才會正確。另外,就像 catch 子句一樣,編譯器可以標示出永遠無法執行到的程式碼來協助你。在這之前,你無法也不需要指定多個 case 之間的評估順序,所以這並不是個破壞性的改變 (breaking change)。由 case ... 引進的 pattern 變數 ,他的可視範圍只限於對應的 switch 區段。
想要從一個 method 傳回一個以上的傳回值是蠻常見的狀況。但是目前 C# 版本對這需求能提供的作法都不夠好。現有的作法有:
out
參數:System.Tuple<...>
型別來傳回值:為了讓這件事做得更好,C# 7.0 新增了 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
}
這 method 現在能更有效率的傳回三個字串型別的傳回值了,這範例將三個字串包成一個 tuple。
呼叫這 method 的程式碼將會收到回傳的 tuple 物件,且能透過 tuple 物件個別存取這些封裝在內的資料:
var names = LookupName(id);
WriteLine($"found {names.Item1} {names.Item3}.");
其中 Item1
等等,為 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 literals 內指定元素的名稱:
return (first: first, middle: middle, last: last); // named tuple elements in a literal
一般來說,你可以互相指派 tuple 的變數,而不用管他的名稱為何: 只要個別的元素都可以被指派,tuple 型別可以自由轉換為其他的 tuple 型別。
Tuples 是 value types, 而且它包含的元素都很單純的被標示為 public
, 都是可異動的欄位 (mutable fields)。它們是 "數值相等" (value equality) 的,
意思是只要兩個 tuples 的所有對應的元素都是相等的 (而且 hash code 也必須相同),那這兩個 tuples 就是相等的 (hash code 也會相同) 。
除了傳回多個傳回值的情況之外,在其他地方 tuples 也很有用。例如,如果你需要一個包含多個 Key 的 Dictionary
,你只需要拿 tuple 當作 Dictionary
的 Key 就可以了。如果你需要在 List
內的一個元素放置多個不同的數值,只要使用 tuple 型別並且搜尋這個 List
。在這些情況中,tuple 都能正常運作。
Tuples 的實作必須依靠底層的泛型結構型別 (generic struct types): ValueTuple<...>
。如果你使用的 target framework 版本還未包含它,你只需要透過 NuGet 取得他們即可:
另一個使用 tuples 的方式是將他們 deconstruct (解構)。Deconstructing declaration (解構宣告) 是用來將 tuple (或是其他值) 裡面的部分拆解並個別指派到其他新的變數用的語法:
(string first, string middle, string last) = LookupName(id1); // deconstructing declaration
WriteLine($"found {first} {last}.");
在 deconstructing declaration (解構宣告) 中,可以在個別的變數上使用 var
:
(var first, var middle, var last) = LookupName(id1); // var inside
甚至你可以在括號外面只用單一一個 var
:
var (first, middle, last) = LookupName(id1); // var outside
你也可以透過 deconstructing assignment (解構指派) 將 tuple 解構後指派到一個既有的變數:
(first, middle, last) = LookupName(id2); // deconstructing assignment
Deconstruction 不只適用於 tuple,任何型別只要它包含 deconstructor (解構式, 無論是定義
在 instance method 或是 extension method 都可以) ,就可以被解構:
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
在這個 deconstructor 裡定義的所有 out
參數,就是該型別物件解構後的所有項目。
(為何在這邊我們使用 out
參數,而不直接傳回 tuple ? 因為這樣就可以讓你為不同數量的
變數,分別定義多個 overloads (多載))
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);
你可以用這樣常見的模式,讓 constructor 與 deconstructor 的參數對稱排列。
就如同 out
變數的語法,我們允許你在 deconstructor 中 "忽略" 你不在意的 out
參數:
(var myX, _) = GetPoint(); // I only care about myX
譯註: 請勿將這裡介紹的 deconstructor 與一般物件導向語言 (如: C++, C# 都有) 常見的 descructor 搞混了。
這個段落介紹的 C# 解構式 (deconstructor), 是定義物件如何 "拆解" 為多個獨立的變數。拆解後原物件仍然存在。
而 C# 與 constructor (建構式) 作用相反的 descructor (解構函式), 則是定義物件要被銷毀前必須執行的動作。
兩者的中文譯名都同樣是 "解構" 請特別留意。
對於 C# descructor 的說明,可以參考: https://msdn.microsoft.com/en-us/library/66x5fx1b.aspx
有時,輔助函式只有在使用他的函式內才有意義。現在這種情況下,你可以在其他函式內宣告 local functions (區域函式):
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);
}
}
在 local function (區域函式) 內,可以直接使用封閉區塊內的 parameters (參數) 與 local variables (區域變數),用法及規則就跟 lambda 運算式 的用法一樣。
舉例來說,iterator method 通常外面都需要包覆另一個 non-iterator method ,用來在呼叫時做參數檢查 (iteraotr 在這時並不會執行,而是在 MoveNext()
被呼叫時才會啟動)。這時 local function 就非常適合在這裡使用:
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; }
}
}
}
同樣的例子,不用 local function 的話,就必須把該 method 定義在 Filter 後面,將 iterator 宣告為 private method。這樣會導致封裝性被破壞: 其他成員可能意外的使用它 (而且參數檢查會被略過)。同時,所有原本 local function 需要取用的區域變數與參數,都必須做一樣的處理 (變成 private members)。
C# 7.0 允許在 number literal (數字常數) 中,用 _
當作 digit separator (數字分隔器):
var d = 123_456;
var x = 0xAB_CD_EF;
你可以將 _
放在數字中的任何位置,來提高程式碼的可讀性,完全不會對數值本身有任何影響。
此外,C# 7.0 也引入二進位的常數表示方式,你現在可以直接用二進位的方式來取代過去十六進位 (例: 0x001234
) 的表示方式。例如:
var b = 0b1010_1011_1100_1101_1110_1111;
如同你可以在 C# 用參考的方式傳遞參數 (使用 ref
修飾詞),你現在也可以用同樣的方式將區域變數的數值用參考的方式傳回。
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
這在回傳大型資料結構時相當有用。舉例來說,遊戲程式可能會預先配置龐大的陣列來存放結構資料 (這樣是為了避免執行過程中發生 garbage collect,
導致遊戲暫停)。現在 method 可以直接用參考的方式傳回結構的資料,呼叫端可以直接讀取與修改它的內容。
同時,有些搭配限制來確保這樣做是安全的:
到目前為止,C# 的非同步 method 限定必須傳回 void
, Task
或是 Task<T>
這幾種型別。C# 7.0 開始,也允許你用同樣的方式,從非同步方法傳回你定義的其他型別。
舉例來說,我們現在可以定義 ValueTask<T>
這個 struct 型別當作傳回值。
這可以避免當非同步執行的結果已經可用,但是卻因為要進行 Task<T>
的配置,而導致非同步執行的結果還在等待中 (awaiting 狀態)。許多涉及 buffering(緩衝) 的非同步操作時,這做法可以明顯地降低配置的次數,同時能帶來明顯的效能提升。
譯註: 例如非同步 I/O 的操作,我們會用非同步的方式將檔案的內容讀到 buffer 內,完成後再不斷重複同樣動作,直到檔案讀取完畢為止,這個動作也許會被重複上千萬次。此時由
Task<T>
替換為ValueTask<T>
可能可以帶來明顯的效能提升。
也有很多其他的情況下,你可以想像自訂 "task-like" 的應用類型會很有用。要正確地建立它們並不是那麼的直觀,所以我們也不期待大部分的人能正確的使用它們。但是它們可能開始會出現在其他的框架或是 API,而呼叫者可以像過去使用 Task
一樣的使用他,傳回值與 await
等待結果。
在 C# 6.0 以前,expression bodied methods, properties(屬性) 等功能大受歡迎,但不是所有的成員都可以
使用。在 C# 7.0 中,accessors (存取子), constructor (建構式) 與 finalizers (終結器) 都已加到可以使用 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
}
}
這個新語法的範例程式並非來自 Microsoft C# 編譯器的團隊,而是由社群成員貢獻的。太棒了! Open source!
要在運算式之中丟出一個例外 (exception) 是很容易的,只要呼叫 method (在 method 內擲出 exception) 就可以了。但是在 C# 7.0 我們允許在運算式之中直接就地丟出 exception:
class Person
{
public string Name { get; }
public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(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();
}
🌟 更多最新文章及資訊 >> MSDN 台灣粉絲專頁 & MSDN 台灣部落格
🌟 更多熱門課程影片 >> Channel 9 免費課程平台
Ask Learn is an AI assistant that can answer questions, clarify concepts, and define terms using trusted Microsoft documentation.
Please sign in to use Ask Learn.
Sign in