1. 程式人生 > >C#9.0新特性詳解系列之六:增強的模式匹配

C#9.0新特性詳解系列之六:增強的模式匹配

自C#7.0以來,模式匹配就作為C#的一項重要的新特性在不斷地演化,這個借鑑於其小弟F#的函數語言程式設計的概念,使得C#的本領越來越多,C#9.0就對模式匹配這一功能做了進一步的增強。 為了更為深入和全面的瞭解模式匹配,在介紹C#9.0對模式匹配增強部分之前,我對模式匹配整體做一個回顧。 ## 1 模式匹配介紹 ### 1.1 什麼是模式匹配? 在特定的上下文中,模式匹配是用於檢查所給物件及屬性是否滿足所需模式(即是否符合一定標準)並從輸入中提取資訊的行為。它是一種新的程式碼流程控方式,它能使程式碼流可讀性更強。這裡說到的標準有“是不是指定型別的例項”、“是不是為空”、“是否與給定值相等”、“例項的屬性的值是否在指定範圍內”等。 模式匹配常結合is表示式用在if語句中,也可用在switch語句在switch表示式中,並且可以用when語句來給模式指定附加的過濾條件。它非常善於用來探測複雜物件,例如:外部Api返回的物件在不同情況下返回的型別不一致,如何確定物件型別? ### 1.2 模式匹配種類 從C#的7.0版本到現在9.0版本,總共有如下十三種模式: * 常量模式(C#7.0) * Null模式(C#7.0) * 型別模式(C#7.0) * 屬性模式(C#8.0) * var模式(C#8.0) * 棄元模式 (C#8.0) * 元組模式(C#8.0) * 位置模式(C#8.0) * 關係模式(C#9.0) * 邏輯模式(C#9.0) * 否定模式(C#9.0) * 合取模式(C#9.0) * 析取模式(C#9.0) * 括號模式(C#9.0) 後面內容,我們就以上這些模式以下面幾個型別為基礎進行寫示例進行說明。 ```C# public readonly struct Point { public Point(int x, int y) => (X, Y) = (x, y); public int X { get; } public int Y { get; } public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); } public abstract record Shape():IName { public string Name =>this.GetType().Name; } public record Circle(int Radius) : Shape,ICenter { public Point Center { get; init; } } public record Square(int Side) : Shape; public record Rectangle(int Length, int Height) : Shape; public record Triangle(int Base, int Height) : Shape { public void Deconstruct(out int @base, out int height) => (@base, height) = (Base, Height); } interface IName { string Name { get; } } interface ICenter { Point Center { get; init; } } ``` ## 2 各模式介紹與示例 ### 2.1 常量模式 常量模式是用來檢查輸入表示式的結果是否與指定的常量相等,這就像C#6.0之前switch語句支援的常量模式一樣,自C#7.0開始,也支援is語句。 ``` expr is constant ``` 這裡expr是輸入表示式,constant是字面常量、列舉常量或者const定義常量變數這三者之一。如果expr和constant都是整型型別,那麼實質上是用expr == constant來決定兩者是否相等;否則,表示式的值通過靜態函式Object.Equals(expr, constant)來決定。 ```C# var circle = new Circle(4); if (circle.Radius is 0) { Console.WriteLine("This is a dot not a circle."); } else { Console.WriteLine($"This is a circle which radius is {circle.Radius}."); } ``` ### 2.2 null模式 null模式是個特殊的常量模式,它用於檢查一個物件是否為空。 ``` expr is null ``` 這裡,如果輸入表示式expr是引用型別時,expr is null表示式使用(object)expr == null來決定其結果;如果是可空值型別時,使用Nullable.HasValue來決定其結果. ```C# Shape shape = null; if (shape is null) { Console.WriteLine("shape does not have a value"); } else { Console.WriteLine($"shape is {shape}"); } ``` ### 2.3 型別模式 型別模式用於檢測一個輸入表示式能否轉換成指定的型別,如果能,把轉換好的值存放在指定型別定義的變數裡。 在is表示式中形式如下: ``` expr is type variable ``` 其中expr表示輸入表示式,type是型別或型別引數名字,variable是型別type定義的新本地變數。如果expr不為空,通過引用、裝箱或者拆箱能轉化為type或者滿足下面任何一個條件,則整個表示式返回值為true,並且expr的轉換結果被賦給變數variable。 * expr是和type一樣型別的例項 * expr是從type派生的型別的例項 * expr的編譯時型別是type的基類,並且expr有一個執行時型別,這個執行時型別是type或者type的派生類。編譯時型別是指宣告變數是使用的型別,也叫靜態型別;執行時型別是定義的變數中具體例項的型別。 * expr是實現了type介面的型別的例項 如果expr是true並且is表示式被用在if語句中,那麼variable本地變數僅在if語句內被分配空間進行賦值,本地變數的作用域是從is表示式到封閉包含if語句的塊的結束位置。 需要注意的是:宣告本地變數的時候,type不能是可空值型別。 ```C# Shape shape = new Square(5); if (shape is Circle circle) { Console.WriteLine($"This shape is a {circle.Name} with radius equal to {circle.Radius}"); } else { Console.WriteLine(circle.Radius);//錯誤,使用了未賦值的本地變數 circle = new Circle(6); Console.WriteLine($"A new {circle.Name} with radius equal to {circle.Radius} is created now."); } //circle變數還處於其作用域內,除非到了封閉if語句的程式碼塊結束的位置。 if (circle is not null && circle.Radius is 0) { Console.WriteLine("This is a dot not a circle."); } else { Console.WriteLine($"This is a circle which radius is {circle.Radius}."); } ``` 上面的包含型別模式的if語句塊部分: ```C# if (shape is Circle circle) { Console.WriteLine($"This shape is a {circle.Name} with radius equal to {circle.Radius}"); } ``` 與下面程式碼是等效的。 ```C# var circle = shape as Circle; if (circle != null) { Console.WriteLine($"This shape is a {circle.Name} with radius equal to {circle.Radius}"); } ``` 從上面可以看出,應用型別模式匹配,使得程式程式碼更為緊湊簡潔。 ### 2.4 屬性模式 屬性模式使你能訪問物件例項的屬性或者欄位來檢查輸入表示式是否滿足指定標準。與is表示式結合使用的基本形式如下: ```C# expr is type {prop1:value1,prop2:value2,...} variable ``` 該模式先檢查expr的執行時型別是否能轉化成型別type,如果不能,這個模式表示式返回false;如果能,則開始檢查其中屬性或欄位的值匹配,如果有一個不相符,整個匹配結果就為false;如果都匹配,則將expr的物件例項賦給定義的型別為type的本地變數variable。 其中, * type可以省略,如果省略,則type使用expr的靜態型別; * 屬性中的value可以為常量、var模式、關係模式或者組合模式。 下面例子用於檢查shape是否是為高和寬相等的長方形,如果是,將其值賦給用Rectangle定義的本地變數rect中: ```C# if (shape is Rectangle { Length: var l,Height:var w } rect && l == w) { Console.WriteLine($"This is a square"); } ``` 屬性模式是可以巢狀的,如下檢查圓心座標是否在原點位置,並且半徑為100: ```C# if (shape is Circle {Radius:100, Center: {X:0,Y:0} c }) { Console.WriteLine("This is a circle which center is at (0,0)"); } ``` 上面示例與下面程式碼是等效的,但是採用模式匹配方式寫的條件程式碼量更少,特別是有更多屬性需要進行條件檢查時,程式碼量節省更明顯;而且上面程式碼還是原子操作,不像下面程式碼要對條件進行4次檢查: ```C# if (shape is Circle circle && circle.Radius == 100 && circle.Center.X == 0 && circle.Center.Y == 0) { Console.WriteLine("This is a circle which center is at (0,0)"); } ``` ### 2.5 var模式 將型別模式表達形式的type改為var關鍵字,就成了var模式的表達形式。var模式不管什麼情況下,甚至是expr計算機結果為null,它都是返回true。其最大的作用就是捕獲expr表示式的值,就是expr表示式的值會被賦給var後的區域性變數名。區域性變數的型別就是表示式的靜態型別,這個變數可以在匹配的模式外部被訪問使用。var模式沒有null檢查,因此在你使用區域性變數之前必須手工對其進行null檢查。 ```C# if (shape is var sh && sh is not null) { Console.WriteLine($"This shape's name is {sh.Name}."); } ``` 將var模式和屬性模式相結合,捕獲屬性的值。示例如下所示。 ```C# if (shape is Square { Side: var side } && side > 0 && side < 100) { Console.WriteLine($"This is a square which side is {side} and between 0 and 100."); } ``` ### 2.6 棄元模式 棄元模式是任何表示式都可以匹配的模式。棄元不能當作常量或者型別直接用於is表示式,它一般用於元組、switch語句或表示式。例子參見2.7和4.3相關的例子。 ```C# var isShape = shape is _; //錯誤 ``` ### 2.7 元組模式 元組模式將多個值表示為一個元組,用來解決一些演算法有多個輸入組合這種情況。如下面的例子結合switch表示式,根據命令和引數值來建立指定圖形: ```C# Shape Create(int cmd, int value1, int value2) =>
(cmd,value1,value2) switch { (0,var v,_)=>new Circle(v), (1,var v,_)=>new Square(v), (2,var l,var h)=>new Rectangle(l,h), (3,var b,var h)=>new Triangle(b,h), (_,_,_)=>throw new NotSupportedException() }; ``` 下面是將元組模式用於is表示式的例子。 ```C# (Shape shape1, Shape shape2) shapeTuple = (new Circle(100),new Square(50)); if (shapeTuple is (Circle circle, _)) { Console.WriteLine($"This shape is a {circle.Name} with radius equal to {circle.Radius}"); } ``` ### 2.8 位置模式 位置模式是指通過新增解構函式將型別物件的屬性解構成以元組方式組織的離散型變數,以便你可以使用這些屬性作為一個模式進行檢查。 例如我們給Point結構中新增解構函式Deconstruct,程式碼如下: ```C# public readonly struct Point { public Point(int x, int y) =>
(X, Y) = (x, y); public int X { get; } public int Y { get; } public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); } ``` 這樣,我就可以將Point結構成不同的變數。 ```C# var point = new Point(10,20); var (x, y) = point; Console.WriteLine($"x = {x}, y = {y}"); ``` 解構函式使物件具有了位置模式的功能,使用的時候,看起來像元組模式。例如我用在is語句中例子如下: ```C# if (point is (10,_)) { Console.WriteLine($"This point is (10,{point.Y})"); } ``` 由於位置型record型別,預設已經帶有解構函式Deconstruct,因此可以直接使用位置模式。如果是class和struct型別,則需要自己新增解構函式Deconstruct。我們也可以用擴充套件方法給一些型別新增解構函式Deconstruct。 ### 2.9 關係模式 關係模式用於檢查輸入是否滿足與常量進行比較的關係約束。形式如: op constant 其中 * op表示操作符,關係模式支援二元操作符:<,<=,>,>= * constant是常量,其型別只要是能支援上述二元關係操作符的內建型別都可以,包括sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, nint和 nuint。 * op的左運算元將做為輸入,其型別與常量型別相同,或者能夠通過拆箱或者顯式可空型別轉換為常量型別。如果不存在轉換,則編譯時會報錯;如果存在轉換,但是轉換失敗,則模式不匹配;如果相同或者能轉換成功,則其值或轉換的值與常量開始進行關係操作運算,該運算結果就是關係模式匹配的結果。由此可見,左運算元可以為dynamic,object,可空值型別,var型別及和constant相同的基本型別等。 * 常量不能是null; * double.NaN或float.NaN雖是常量,但不是數字,是不受支援的。 * 該模式可用在is,which語句和which表示式中。 ```C# int? num1 = null; const int low = 0; if (num1 is >low) { } ``` 關係模式與邏輯模式進行結合,功能就會更加強大,幫助我們處理更多的問題。 ```C# int? num1 = null; const int low = 0; double num2 = double.PositiveInfinity; if (num1 is >low and
0 && square.Side < 100) { Console.WriteLine($"This shape is a square with a side {square.Side}"); } } ``` 現在,我們可以用模式匹配將上述邏輯描述為: ```C# if (shape is Square { Side: > 0 and < 100 } square) { Console.WriteLine($"This shape is a square with a side {square.Side}"); } ``` 這裡,我們將一個型別模式、一個屬性模式、一個合取模式、兩個關係模式和兩個常量模式進行組合。兩段同樣效果的程式碼,明顯模式匹配程式碼量更少,沒了square.Side的重複出現,更為簡潔易懂。 注意事項: * and要用於兩個型別模式之間,則兩個型別必須有一個是介面,或者都是介面 ```C# shape is Square and Circle // 編譯錯誤 shape is Square and IName // Ok shape is IName and ICenter // OK ``` * and不能用在一個沒有關係模式的屬性模式中, ```C# shape is Circle { Radius: 0 and 10 } // 編譯錯誤 ``` * and不能用在兩個屬性模式之間,因為這已經隱式實現了 ```C# shape is Triangle { Base: 10 and Height: 20 } // 編譯錯誤 shape is Triangle { Base: 10 , Height: 20} // OK,是上一句要實現的效果 ``` #### 2.10.3 析取模式 類似於邏輯操作符||,析取模式就是用or關鍵詞連線兩個模式,要求兩個模式中有一個能匹配就算匹配成功。 例如下面程式碼用來檢查一個圖形是否是邊長小於20或者大於60的有效的正方形: ```C# if (shape is Square { Side: >0 and < 20 or > 60 } square) { Console.WriteLine($"This shape is a square with a side {square.Side}"); } ``` 這裡,我們組合運用了型別模式、屬性模式、合取模式、析取模式、關係模式和常量模式這六個模式來完成條件判斷。看起來很簡潔,這個如果用C#9.0之前的程式碼實現如下,繁瑣很多,並且square.Side有重複出現: ```C# if (shape is Square) { var square = shape as Square; if (square.Side > 0 && square.Side < 20 || square.Side>60) { Console.WriteLine($"This shape is a square with a side {square.Side}"); } } ``` 注意事項: * or 可以放在兩個型別之間,但是不支援捕捉輸入表示式的值存到定義的區域性變數裡; ```C# shape is Square or Circle // OK shape is Square or Circle smt // 編譯錯誤,不支援捕捉 ``` * or 可以放在一個沒有關係模式的屬性模式中,同時支援捕捉輸入表示式的值存到定義的區域性變數裡 ```C# shape is Square { Side: 0 or 1 } sq // OK ``` * or 不能用於同一物件的兩個屬性之間 ```C# shape is Rectangle { Height: 0 or Length: 0 } // 編譯錯誤 shape is Rectangle { Height: 0 } or Rectangle { Length: 0 } // OK,實現了上一句想實現的目標 ``` ### 2.11 括號模式 有了以上各種模式及其組合後,就牽扯到一個模式執行優先順序順序的問題,括號模式就是用來改變模式優先順序順序的,這與我們表示式中括號的使用是一樣的。 ```C# if (shape is Square { Side: >0 and (< 20 or > 60) } square) { Console.WriteLine($"This shape is a square with a side {square.Side}"); } ``` ## 3 其他 有了模式匹配,對於是否為null的判斷檢查,就顯得豐富多了。下面這些都可以用於判斷不為null的程式碼: ```C# if (shape != null)... if (!(shape is null))... if (shape is not null)... if (shape is {})... if (shape is {} s)... if (shape is object)... if (shape is object s)... if (shape is Shape s)... ``` ## [4][1] switch語句與表示式中的模式匹配 說到模式匹配,就不得不提與其緊密關聯的switch語句、switch表示式和when關鍵字。 ### 4.1 when關鍵字 when關鍵字是在上下文中用來進一步指定過濾條件。只有當過濾條件為真時,後面語句才得以執行。 被用到的上下文環境有: * 常用在try-catch或者try-catch-finally語句塊的catch語句中 * 用在switch語句的case標籤中 * 用在switch表示式中 這裡,我們重點介紹後面兩者情況,有關在catch中的應用,如有不清楚的可以查閱相關資料。 在switch語句的when的使用語法如下: > case (expr) when (condition): 這裡,expr是常量或者型別模式,condition是when的過濾條件,可以是任何的布林表示式。具體示例見後面switch語句中的例子。 在switch表示式中when的應用與switch類似,只不過case和冒號被用=>替代而已。具體示例見switch語句表示式。 ### 4.2 switch語句 自C#7.0之後,switch語句被改造且功能更為強大。變化有: * 支援任何型別 * case可以用表示式,不再侷限於常量 * 支援匹配模式 * 支援when關鍵字進一步限定case標籤中的表示式 * case之間不再相互排斥,因而case的順序很重要,執行匹配了第一個分支,後面分支都會被跳過。 下面方法用於計算指定圖形的面積。 ```C# static int ComputeArea(Shape shape) { switch (shape) { case null: throw new ArgumentNullException(nameof(shape)); case Square { Side: 0 }: case Circle { Radius: 0 }: case Rectangle rect when rect is { Length: 0 } or { Height: 0 }: case Triangle { Base: 0 } or Triangle { Height: 0 }: return 0; case Square { Side:var side}: return side * side; case Circle c: return (int)(c.Radius * c.Radius * Math.PI); case Rectangle { Length:var l,Height:var h}: return l * h; case Triangle (var b,var h): return b * h / 2; default: throw new ArgumentException("shape is not a recognized shape",nameof(shape)); } } ``` 上面該方法僅用於展示模式匹配多種不同可能的用法,其中計算面積為0的那一部分其實是沒有必要的。 ### 4.3 switch表示式 switch表示式是為在一個表示式的上下文中可以支援像switch語句那樣的功能而新增的表示式。 我們將4.1中的switch語句改為表示式,如下所示: ```C# static int ComputeArea(Shape shape) => shape switch { null=> throw new ArgumentNullException(nameof(shape)), Square { Side: 0 } => 0, Rectangle rect when rect is { Length: 0 } or { Height: 0 } => 0, Triangle { Base: 0 } or Triangle { Height: 0 } => 0, Square { Side: var side } => side*side, Circle c => (int)(c.Radius * c.Radius * Math.PI), Rectangle { Length: var l, Height: var h } => l * h, Triangle (var b, var h) => b * h / 2, _=> throw new ArgumentException("shape is not a recognized shape",nameof(shape)) }; ``` 由上例子可以看出,switch表示式與switch語句有以下不同: * 輸入引數位於switch關鍵字前面 * case和:被用=>替換,顯得更加簡練和直觀 * default被棄元符號_替代 * 語句體是表示式不是語句 switch表示式的每個分支=>標記後面的表示式們的最佳公共型別如果存在,並且每個分支的表示式都可以隱式轉換為這個型別,那麼這個型別就是switch表示式的型別。 在執行情況下,switch表示式的結果是輸入引數第一個匹配到的模式的分支中表達式的值。如果沒有匹配到的情況,就會丟擲SwitchExpressionException異常。 switch表示式的各個分支情況要全面覆蓋輸入引數的各種值的情況,否則會報錯。這也是棄元在switch表示式中用於代表不可知情況的原因。 如果switch表示式中一些前面分支總是得到匹配,不能到達後面的分支話,就會出錯。這就是棄元模式要放在最後分支的原因。 ## 5 為什麼用模式匹配? 從前面很多例子可以看出,模式匹配的很多功能都可以用傳統方法實現,那麼為什麼還要用模式匹配呢? 首先,就是我們前面提到的模式匹配程式碼量少,簡潔易懂,減少程式碼重複。 再者,就是模式常量表達式在運算時是原子的,只有匹配或者不匹配兩種相斥的情況。而多個連線起來的條件比較運算,要多次進行不同的比較檢查。這樣,模式匹配就避免了在多執行緒場景中的一些問題。 總的來說,如果可能的話,請使用模式匹配,這才是最佳實踐。 ## [6][1] 總結 這裡我們回顧了所有的模式匹配,也介紹了模式匹配在switch語句和switch表示式中的使用情況,最後介紹了為什麼使用模式匹配的原因。 [1]: https://mp.weixin.qq.com/s?__biz=MzAwNjcyNTU2Ng==&mid=2247483816&idx=1&sn=ee2e74ece4f5b69620ce50fae8b3072a&chksm=9b084b79ac7fc26f0d3ccf4440c23b531ff48fd97b9d0b24a5f8fd73db6e50382c3cb80f77fb&scene=178&cur_album_id=1612459507345899521#rd #### 如對您有價值,請推薦,您的鼓勵是我繼續的動力,在此萬分感謝。關注本人公眾號“碼客風雲”,享第一時間閱讀最新文章。