自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)
后面內容,我們就以上這些模式以下面幾個型別為基礎進行寫示例進行說明,
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)來決定,
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
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不能是可空值型別,
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陳述句塊部分:
if (shape is Circle circle)
{
Console.WriteLine($"This shape is a {circle.Name} with radius equal to {circle.Radius}");
}
與下面代碼是等效的,
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運算式結合使用的基本形式如下:
expr is type {prop1:value1,prop2:value2,...} variable
該模式先檢查expr的運行時型別是否能轉化成型別type,如果不能,這個模式運算式回傳false;如果能,則開始檢查其中屬性或欄位的值匹配,如果有一個不相符,整個匹配結果就為false;如果都匹配,則將expr的物件實體賦給定義的型別為type的本地變數variable,
其中,
- type可以省略,如果省略,則type使用expr的靜態型別;
- 屬性中的value可以為常量、var模式、關系模式或者組合模式,
下面例子用于檢查shape是否是為高和寬相等的長方形,如果是,將其值賦給用Rectangle定義的本地變數rect中:
if (shape is Rectangle { Length: var l,Height:var w } rect && l == w)
{
Console.WriteLine($"This is a square");
}
屬性模式是可以嵌套的,如下檢查圓心坐標是否在原點位置,并且半徑為100:
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次檢查:
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檢查,
if (shape is var sh && sh is not null)
{
Console.WriteLine($"This shape's name is {sh.Name}.");
}
將var模式和屬性模式相結合,捕獲屬性的值,示例如下所示,
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相關的例子,
var isShape = shape is _; //錯誤
2.7 元組模式
元組模式將多個值表示為一個元組,用來解決一些演算法有多個輸入組合這種情況,如下面的例子結合switch運算式,根據命令和引數值來創建指定圖形:
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運算式的例子,
(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,代碼如下:
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結構成不同的變數,
var point = new Point(10,20);
var (x, y) = point;
Console.WriteLine($"x = {x}, y = {y}");
解構函式使物件具有了位置模式的功能,使用的時候,看起來像元組模式,例如我用在is陳述句中例子如下:
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運算式中,
int? num1 = null;
const int low = 0;
if (num1 is >low)
{
}
關系模式與邏輯模式進行結合,功能就會更加強大,幫助我們處理更多的問題,
int? num1 = null;
const int low = 0;
double num2 = double.PositiveInfinity;
if (num1 is >low and <int.MaxValue && num2 is <double.PositiveInfinity)
{
}
2.10 邏輯模式
邏輯模式用于處理多個模式間邏輯關系,就像邏輯運算子!、&&和||一樣,優先級順序也是相似的,為了避免與運算式邏輯運算子引起混淆,模式運算子采用單詞來表示,他們分別為not、and和or,邏輯模式為多個基本模式進行組合提供了更多可能,
2.10.1 否定模式
否定模式類似于!運算子,用來檢查與指定的模式不匹配的情況,它的關鍵字是not,例如null模式的否定模式就是檢查輸入運算式不為null.
if (shape is not null)
{
// 當shape不為null時的代碼邏輯
Console.WriteLine($"shape is {shape}.");
}
上面這段代碼我們將否定模式與null模式組合了起來,實作了與下面代碼等效的功能,但是易讀性更好,
if (!(shape is null))
{
// 當shape不為null時的代碼邏輯
Console.WriteLine($"shape is {shape}.");
}
我們可以將否定模式與型別模式、屬性模式、常量模式等結合使用,用于更多的場景,例如下面例子就將型別模式、屬性模式、否定模式和常量模式四種組合起來檢查一個圖形是否是一個半徑不為零的圓,
if (shape is Circle { Radius: not 0 })
{
Console.WriteLine("shape is not a dot but a Circle");
}
下面示例判斷一個shape如果不是Circle時執行一段邏輯,
if (shape is not Circle circle)
{
Console.WriteLine("shape is not a Circle");
}
注意:上面這段代碼,如果if判斷條件為true的話,那么circle的值為null,不能在if陳述句塊中使用,但為false時,circle不為null,即使在if陳述句塊中得到了使用,但也得不到執行,只能在if陳述句后面使用,
2.10.2 合取模式
類似于邏輯運算子&&,合取模式就是用and關鍵詞連接兩個模式,要求他們都同時匹配,
以前,我們檢查一個物件是否是邊長位于(0,100)之間的正方形時,會有如下代碼:
if (shape is Square)
{
var square = shape as Square;
if (square.Side > 0 && square.Side < 100)
{
Console.WriteLine($"This shape is a square with a side {square.Side}");
}
}
現在,我們可以用模式匹配將上述邏輯描述為:
if (shape is Square { Side: > 0 and < 100 } square)
{
Console.WriteLine($"This shape is a square with a side {square.Side}");
}
這里,我們將一個型別模式、一個屬性模式、一個合取模式、兩個關系模式和兩個常量模式進行組合,兩段同樣效果的代碼,明顯模式匹配代碼量更少,沒了square.Side的重復出現,更為簡潔易懂,
注意事項:
- and要用于兩個型別模式之間,則兩個型別必須有一個是介面,或者都是介面
shape is Square and Circle // 編譯錯誤
shape is Square and IName // Ok
shape is IName and ICenter // OK
- and不能用在一個沒有關系模式的屬性模式中,
shape is Circle { Radius: 0 and 10 } // 編譯錯誤
- and不能用在兩個屬性模式之間,因為這已經隱式實作了
shape is Triangle { Base: 10 and Height: 20 } // 編譯錯誤
shape is Triangle { Base: 10 , Height: 20} // OK,是上一句要實作的效果
2.10.3 析取模式
類似于邏輯運算子||,析取模式就是用or關鍵詞連接兩個模式,要求兩個模式中有一個能匹配就算匹配成功,
例如下面代碼用來檢查一個圖形是否是邊長小于20或者大于60的有效的正方形:
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有重復出現:
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 可以放在兩個型別之間,但是不支持捕捉輸入運算式的值存到定義的區域變數里;
shape is Square or Circle // OK
shape is Square or Circle smt // 編譯錯誤,不支持捕捉
- or 可以放在一個沒有關系模式的屬性模式中,同時支持捕捉輸入運算式的值存到定義的區域變數里
shape is Square { Side: 0 or 1 } sq // OK
- or 不能用于同一物件的兩個屬性之間
shape is Rectangle { Height: 0 or Length: 0 } // 編譯錯誤
shape is Rectangle { Height: 0 } or Rectangle { Length: 0 } // OK,實作了上一句想實作的目標
2.11 括號模式
有了以上各種模式及其組合后,就牽扯到一個模式執行優先級順序的問題,括號模式就是用來改變模式優先級順序的,這與我們運算式中括號的使用是一樣的,
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的代碼:
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 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的順序很重要,執行匹配了第一個分支,后面分支都會被跳過,
下面方法用于計算指定圖形的面積,
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陳述句改為運算式,如下所示:
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 總結
這里我們回顧了所有的模式匹配,也介紹了模式匹配在switch陳述句和switch運算式中的使用情況,最后介紹了為什么使用模式匹配的原因,
如對您有價值,請推薦,您的鼓勵是我繼續的動力,在此萬分感謝,關注本人公眾號“碼客風云”,享第一時間閱讀最新文章,
<iframe style="background: rgba(255, 255, 255, 1)" src="https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1612459507345899521&__biz=MzAwNjcyNTU2Ng==#wechat_redirect" frameborder="0" width="100%" height="342"></iframe>
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/234526.html
標籤:C#
