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版本,總共有以下十三種模式:app

  • 常量模式(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)

後面內容,咱們就以上這些模式如下面幾個類型爲基礎進行寫示例進行說明。ide

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模式是個特殊的常量模式,它用於檢查一個對象是否爲空。ui

expr is null

這裏,若是輸入表達式expr是引用類型時,expr is null表達式使用(object)expr == null來決定其結果;若是是可空值類型時,使用Nullable .HasValue來決定其結果. this

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表達式中的使用狀況,最後介紹了爲何使用模式匹配的緣由。

如對您有價值,請推薦,您的鼓勵是我繼續的動力,在此萬分感謝。關注本人公衆號「碼客風雲」,享第一時間閱讀最新文章。

碼客風雲
相關文章
相關標籤/搜索