C#中的模式匹配

  C#从7.0开始,陆陆续续推出了各种模式匹配,模式是一种特殊的表达式,通过判断给定的值是否满足此表达式而返回true或者false,它就类似于正则表达式的作用。

  目前(C#10),可以使用模式匹配的地方有三个:

  1、is表达式,从C#7.0开始,is表达式的右边不在只是一个类型,而是一个模式,具体例子可见:C#中的is和as的用法

  2、switch语句

  3、switch表达式。从C#7.0开始,每个case语句不在是一个值,而是一个模式,具体例子可见:C#的switch的用法

  现在,C#9.0版本后,已存在9中匹配模式:声明模式、类型模式、常量模式、关系模式、逻辑模式、属性模式、位置模式、var 模式、弃元模式。

  

声明模式

  声明模式用于与检测表达式运行时的类型是否与指定的类型相匹配,如果匹配成功,则将匹配的结果给到指定声明的变量。

  匹配判断成功的条件:

  1. 表达式运行时的类型与模式指定类型相同
  2. 表达式运行时的类型派生自模式指定类型,或者实现模式指定模型接口,换句话说,就是表达式运行时的类型可通过隐式引用装换成模式指定类型,隐式引用装换可参考C#中的隐式转换
  3. 表达式运行时的类型是模式指定类型的可为null的值类型
  4. 表达式运行时的类型可通过装箱或者取消装箱转换成模式指定类型

  一个简单的例子:  

复制代码
    static void Main(string[] args)
    {
        {
            //1.表达式运行时的类型与模式指定类型相同
            object obj = new Exception(nameof(Exception));
            if (obj is Exception exception)
            {
                Console.WriteLine(exception.Message);
            }
        }
        {
            //2.表达式运行时的类型可通过隐式引用装换成模式指定类型
            object obj = new ArgumentNullException();
            if (obj is Exception exception)
            {
                Console.WriteLine(exception.Message);
            }
        }
        {
            //3.表达式运行时的类型是模式指定类型的可为null的值类型
            int? nullable_i = 1;
            if (nullable_i is int i)
            {
                Console.WriteLine(i);
            }
        }
        {
            //4.表达式运行时的类型可通过装箱或者取消装箱转换成模式指定类型
            object obj = true;
            if (obj is bool b)
            {
                Console.WriteLine(b);
            }
        }
    }
复制代码

类型模式

  从C#9.0开始支持类型模式,类型模式是声明模式的一种简单化用法,只是检测表达式运行时的类型是否与指定的类型相匹配,而不需要将匹配成功后的结果给到一个变量,如:

复制代码
    static void Print(object value)
    {
        switch (value)
        {
            case int: Console.WriteLine("value is int"); break;
            case double: Console.WriteLine("value is double"); break;
            case string: Console.WriteLine("value is string"); break;
            case bool: Console.WriteLine("value is bool"); break;
            case object: Console.WriteLine("value is object"); break;
        }
    }
复制代码

常量模式

  常量模式用于测试表达式结果是否等于指定常量,这几乎就是C#6.0及之前版本中switch-case的用法,它可指定的常量类型包括:

  1. 整型(int、long、byte等)和浮点型(float、double、decimal等)
  2. 字符(char)和字符串(string)
  3. 布尔值 true 或 false
  4. 枚举类型值
  5. const声明的常量
  6. null

  例如:  

复制代码
    const string STR = "STR";
    static void Print(object value)
    {
        switch (value)
        {
            case 1: Console.WriteLine(1); break;
            case 1.1: Console.WriteLine(1.1); break;
            case "string": Console.WriteLine("string"); break;
            case true: Console.WriteLine(true); break;
            case false: Console.WriteLine(false); break;
            case DayOfWeek.Saturday: 
            case DayOfWeek.Sunday: Console.WriteLine("weekend"); break;
            case STR: Console.WriteLine(STR); break;
            case null: Console.WriteLine("null"); break;
        }
    }
复制代码

  注:对null的检测不会调用用户重载的相等运算符 ==

关系模式

  关系模式用于将表达式结果与指定常量表达式进行比较,可以使用的关系运算符有:<、>、<= 、>=,而右边的常量表达式只能是:整型(int、long、byte等)、浮点型(float、double、decimal等)、字符(char)、枚举类型值。 

复制代码
    static void Print(double value)
    {
        switch (value)
        {
            case < 0: Console.WriteLine("value is negative"); break;
            case 0: Console.WriteLine("value is 0"); break;
            case > 0: Console.WriteLine("value is positive"); break;
            case double.NaN: Console.WriteLine("value is NaN"); break;
        }
    }
复制代码

逻辑模式

  模式可以通过逻辑关系进行组合,逻辑模式用于检测表达式的结果是否与组合的模式匹配。组合逻辑使用关键字:and、or、not,例如:  

复制代码
    static void Print(object value)
    {
        switch (value)
        {
            case 0: Console.WriteLine("value is 0"); break;
            case not 0 and (100 or -100): Console.WriteLine("abs(value)==100"); break;
            case not 0 and (> 0 and < 100): Console.WriteLine("value is positive and less than 100"); break;
            case not 0 and > 0: Console.WriteLine("value is positive and greater than 100"); break;
            case < -100 or (< 0 and > -100): Console.WriteLine("value is negative and not equals -100"); break;
        }
    }
复制代码

  注:and 优先级高于 or,我们可以使用括号来改变优先级

属性模式

  属性模式用于提取表达式结果的属性或者字段,用于检测表达式结果是否满足指定的属嵌套模式匹配相匹配。

  其实形式上,属性模式使用属性或者字段作为匹配对象,比如:  

复制代码
    static void Main(string[] args)
    {
        DateTime time = DateTime.Now;
        switch (time)
        {
            case { Year: 2021, Month: 7, Day: 12 } t:Console.WriteLine($"Year: {t.Year}, Month: {t.Month}, Day: {t.Day}");break;
            case { Year: 2021, Month: 7 }: Console.WriteLine($"Year: {time.Year}, Month: {time.Month}"); break;
            case { Date: { Year: 2021, Month: 7 } }: Console.WriteLine($"recursion"); break;//使用嵌套递归
        }
    }
复制代码

  可以看到,属性模式用于说明表达式的结果是否具有指定的结构,且对应的属性应该满足相对应的条件。 

  另外,需要注意的是,匹配的每个属性及字段的值是一个匹配模式,如:  

复制代码
    static void Main(string[] args)
    {
        DateTime time = DateTime.Now;
        switch (time)
        {
            case { Year: 2020 or 2021, Month: <= 6, Day: 1 } t: Console.WriteLine($"the first day of every month in the first half of 2020 and 2021"); break;
            case { Year: not 2022 }: Console.WriteLine($"not 2022"); break;
            case { DayOfWeek: not DayOfWeek.Sunday and not DayOfWeek.Saturday }: Console.WriteLine($"recursion"); break;
        }
    }
复制代码

  基于这一点,属性模式可以和声明模式、类型模式等进行组合,用于对属性、字段等进行条件匹配:  

    DateTime time = DateTime.Now;
    if (time is DateTime { Year: >= 2021 })
    {
        //something
    }

位置模式

  位置模式的作用与属性模式一样,只是位置模式借助元组(解构表达式)的相关特性来提取属性或者字段,用于检测表达式结果是否满足指定的属嵌套模式匹配相匹配。

  同时,属性模式中,会要求模式中的属性在匹配对象中显式的存在,否则编译报错,而位置模式因为基于解构,因此没有这种限制。

  需要注意的是,元组中接收的每一项都是模式,如:  

复制代码
    public class Point2D
    {
        public int X { get; set; }
        public int Y { get; set; }
        public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
    }
    static void Print(object point)
    {
        if (point is (> 0, > 0))
        {
        //something
        }
    }
复制代码

  在使用方面,位置模式可以同声明模式、类型模式、属性模式等结合使用:  

复制代码
    public class Point2D
    {
        public int X { get; set; }
        public int Y { get; set; }
        public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
    }
    public class Point3D
    {
        public int X { get; set; }
        public int Y { get; set; }
        public int Z { get; set; }
        public void Deconstruct(out int x, out int y, out int z) => (x, y, z) = (X, Y, Z);
    }
    static void Print(object point)
    {
        if (point is Point2D(> 0, > 0))
        {
            //something
        }
        if (point is Point3D(> 0, > 0, > 0) { X: < 100, Y: < 100, Z: < 100 })
        {
            //something
        }
    }
复制代码

Var模式

  var模式用户提取属性模式、位置模式等模式中的属性、字段等数据到指定的变量中,从而可以进行进一步的匹配或者使用。

  例如:  

复制代码
    public class Point2D
    {
        public int X { get; set; }
        public int Y { get; set; }
        public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
    }
    static void Print(object point)
    {
        //位置模式中使用var
        //(var x, var y)也可以写成 var (x, y)
        if (point is (var x, var y))
        {
            Console.WriteLine($"X:{x}   Y:{y}");
        }

        //属性模式中使用var
        if (point is Point2D { X: var a, Y: var b })
        {
            Console.WriteLine($"X:{a}   Y:{b}");
        }
    }
复制代码

弃元模式

  弃元模式使用下划线(_)作为变量,告诉编译器,这个变量被丢弃了,不再使用(这个弃元可能来源于golang中弃元的思想)。

  弃元很有用,比如类型模式等价于在声明模式中声明弃元:  

复制代码
    static void Main(string[] args)
    {
        object obj = 1;
        bool b = obj is int;
        //等价于
        bool b = obj is int _;
    }
复制代码

  在位置模式和属性模式中也可以使用弃元:  

复制代码
    public class Point2D
    {
        public int X { get; set; }
        public int Y { get; set; }
        public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
    }
    static void Print(object point)
    {
        //位置模式中使用弃元
        //(var x, _)也可以写成 var (x, _)
        if (point is (var x, _))
        {
            Console.WriteLine($"X:{x}");
        }

        //属性模式中使用弃元
        if (point is Point2D { X: var a, Y: _ })
        {
            Console.WriteLine($"X:{a}");
        }
    }
复制代码

  特别是元组,因为解构表达式的存在,因此在解构时,不得不接收每一项,而有时我们可能只关心其中的几项,这时弃元就爬上用场了:  

    (var a, _) = new Tuple<int, int>(1, 2);
    (_, var y) = new Point2D();

  更多弃元的用法参考:C#中的弃元

  参考文档:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns

  

 

出处:https://www.cnblogs.com/shanfeng1000/p/14963188.html

=======================================================================================

C# 模式匹配全解:原理、用法与易错点

引言#

随着C#不断发展,"模式匹配"(Pattern Matching)已经成为让代码更加友好、可读和强大的核心特性。从 C# 7.0 初次引入,到 C# 11的能力扩展,模式匹配为处理类型判断、属性解构、集合匹配等提供了简洁、高效且类型安全的表达方式。它不仅能让 if/switch 等控制结构变得“声明式”,还能带来性能提升。在这篇文章里,我们将深入剖析 C 的所有模式匹配语法和用法,追踪其演变,讲清一些容易混淆和误用的地方,让大家能了解模式匹配本质。


1. 什么是模式匹配#

模式匹配本质上是一种表达式判定工具:用以检查一个对象是否与某种“模式”相吻合,如果吻合,还允许对其分解、绑定成员变量。这可以是类型检查、常量判断、属性结构匹配等。

传统C#写法:

if (person != null && person.Age >= 18 && person is Employee
    && ((Employee)person).YearsAtCompany > 5) {
    // 处理逻辑
}

模式匹配写法:

if (person is Employee { Age: >= 18, YearsAtCompany: > 5 }) {
    // 处理逻辑
}

更简洁、可读、类型安全,不需要重复显式强制转型。


2. C#7.0 初代模式匹配特性#

2.1 Null 模式与常量模式#

C#7.0 首先支持了用is直接与null以及常量对比:

if (obj is null) Console.WriteLine("对象为null");
if (d is Math.PI) Console.WriteLine("d 是圆周率");
if (str is "test") Console.WriteLine("str内容等于test");

注意:常量匹配仅支持编译期常量,比如数字、字符串、bool、enum、const字段和null。不能直接用非const变量或表达式。

这个限制为何存在?#

  • 编译器要保证 switch 和 if 全覆盖检查,如果允许变量参与,则难推导 exhaustiveness(全覆盖)。
  • 也可避免不可预期的副作用与逻辑混乱(比如变量运行期改变等)。

2.2 类型模式与变量捕获#

类型模式允许 you not only 判断类型,还能直接捕获变量:

if (shape is Circle circle1) 
    Console.WriteLine($"圆的半径为 {circle1.Radius}");

甚至可以链式判断:

if (shape is Rectangle r && r.Width == r.Height)
    Console.WriteLine("正方形");

非常适用于临时变量创建、减少强制类型转换(显式as/cast)代码杂音。

is vs as#

  • is 结合新模式后可以直接安全声明变量,无需后续 null 检查。
  • as 后还得写 if (x != null)

2.3 Discard 与 var 模式#

  • Discard(_):匹配但忽略,用于 switch 的 default 分支或类型“只是判断,不关心值”。
  • var:总是匹配成功,并引出变量。通常用于解构场景,比如 switch/case 匹配对象成员。
if (obj is var o)
   Console.WriteLine(o); // 不管 obj 是否为null,o 指向原始值(即使null)

⚠️ 这里容易误用!#

不要用 var 跳过 null 检查。因为这样会把 null 值“吞掉”,造成 NullReferenceError 隐患。

if (p is var _)  
{             // 这里仍可能是 null  
    Console.WriteLine(p.Length); // NullReferenceException  
}  

2.4 switch 语句的模式匹配#

传统 switch 只能匹配简单的枚举或数字等,C#7.0 后支持以下写法:

switch (seq) {
    case Array a: return a.Length;
    case ICollection<T> c: return c.Count;
    case IEnumerable<T> _: return seq.Count();
    default: return 0;
}
  • 某类型满足条件即分支命中
  • Discard case IEnumerable<T> _:
  • default处理剩余情况

switch 的 when 子句#

when 只能用于 switch:

case Array a when a.Length < 10: return a.Length;

无法用于 if


3. C#8.0: Switch表达式、属性、位置、元组模式#

3.1 表达式 Switch#

C#8 引入 switch-表达式,极大提升 switch 可读性和函数式编程体验:

string whatShape = shape switch {
    Circle c     => $"circle radius: {c.Radius}",
    Rectangle _  => "rectangle",
    _            => "null or unknown"
};
  • 匹配分支是表达式,不能有多条语句

=>右边直接返回值,非常适合表达式体成员:

public static string Describe(this Shape s) => s switch { ... };

3.2 属性模式 (Property Patterns)#

可直接针对属性表达式判定

if (shape is Circle { Radius: 1.0 })
    Console.WriteLine("单位圆");

whatShape = shape switch {
    Rectangle { Width: 10, Height: 5 } => "10x5 矩形",
    Rectangle { Width: var x, Height: var y } => $"rect: {x}x{y}",
    { } => "非null",
    _ => "null"
};

重点说明:#

  • { } 代表对象非null。
  • { Width: 10, Height: 5 } 同时要求2个属性等于指定值。
  • { Width: var x, Height: >5 } 可以对属性用关系操作和抽取变量。
  • 若对象有Deconstruct方法,可用位置/元组模式更优雅。

3.3 位置模式(Positional Patterns)#

利用 Deconstruct 抽取变量:

public void Deconstruct(out double w, out double h) => (w, h) = (Width, Height);

shape switch {
    Rectangle(var x, var y) => $"矩形尺寸:{x}x{y}",
    ...
}
  • 用于解构类对象(类似元组)
  • 写法类似解构元组

3.4 元组模式#

处理多个参数的模式组合:

(c1, c2, c3) switch {
    (Color.Blue, Color.White, Color.Red) => "France",
    (Color.Green, Color.White, Color.Red) => "Italy",
    _ => "Unknown"
};
  • 有效解决“复杂组合条件冗长”的老大难。
  • 更直观表达多变量匹配,不需层层嵌套。

4. C#9.0: 组合、括号及关系模式#

4.1 组合模式(and/or/not)#

组合条件极其强大:

c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ','

等价于:

(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '.' || c == ','

not 语法#

if (obj is not null) { ... }

比传统 !(obj is null) 更直观。

4.2 关系模式(Relational Patterns)#

直接用 >, <, >=, <= 表达区间

rate = monthlyIncome switch {
    >=0 and <1000 => 0,
    <5000 => 10,
    _ => 20
};

结合属性/类型模式:

shape switch {
    Circle { Radius: >1 and <=10 } => "较合理圆",
    Circle { Radius: >10 } => "过大圆",
    Rectangle => "矩形",
    _ => "不匹配"
};

4.3 类型模式精简#

C#9 类型模式无需 underscore,eg:Rectangle 直接写。


5. C#10.0: 扩展属性模式#

支持嵌套点表达式:

之前版本:

x is Person { FirstName: { Length: <=5 } }

C#10:

x is Person { FirstName.Length: <=5 }

让属性链判定更加直观简洁。


6. C#11.0: 列表与切片模式#

6.1 列表模式#

让数组/集合结构模式判定成为可能:

if (arr is [1, 2, 3])
// 匹配长度为3,内容依次为1,2,3
  • [..]: 切片模式,等价于“0个或多个元素”
  • _: 忽略一个元素(discard)
[_, >0, ..]      // 至少2个元素,第二个>0
[.., <=0, _]     // 至少2个元素,倒数第二个<=0且不关心最后一个

注意:切片只能出现一次。

6.2 列表递归#

可以做嵌套匹配:

bool EndsWithSingleIntList(List<List<int>> lists) => lists is [.., [_]];
// 至少一个元素,且最后一个是单元素list

6.3 支持的结构#

只要类型有 Length/Count 属性和支持 [index] 则可用列表模式,如 string、数组、自定义集合(只需这两个成员)。

例如处理国别码:

switch (s) {
    case [char c0, char c1]: // 即 s.Length==2
        ...
}

7. 模式匹配背后的实现与性能分析#

7.1 不是“魔法”,而是IL优化#

许多人以为 pattern matching 是一种基于反射或者表达式树的黑魔法。实际上,编译器常常能把很多 pattern 匹配转为高效的“顺序判断”或“跳表”等机器码。

例如:

c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ','

编译后 Equals:

(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '.' || c == ','

对于一组常量匹配:

statusCode is 0x1000 or 0x1001 or 0x1002 or 0x2000 or 0x2001 or 0x3000

编译器会变换成:

return ((uint)(statusCode - 4096) <= 2u || (uint)(statusCode - 8192) <= 1u || statusCode == 12288);

即用范围短路、减少 equals 判断次数。

⚠️ 建议:#

对于性能敏感代码,请结合 Benchmark.NET 做实测对比。有时手写的判定和 pattern matching 效果类似,甚至后者更优。


8. 常见误区与最佳实践#

8.1 不要用 pattern 替代多态#

最容易见的误用是用模式匹配处理继承结构的多态行为:

// xxx
public static double Area(this Shape shape) => shape switch {
    Circle c    => c.Radius * c.Radius * Math.PI,
    Rectangle r => r.Width * r.Height
};

事实上,应该优先用抽象基类/接口的虚方法或属性

abstract class Shape { public abstract double Area {get;} }

class Circle : Shape {
    public double Radius { get; set; }
    public override double Area => Radius * Radius * Math.PI;
}

为什么?

  • 可扩展性强(新子类无需修改 Area 逻辑,符合开闭原则)。
  • 虚调用比类型判定快。
  • 更少维护负担。

8.2 Constant-Only 限制#

只有编译期常量才能用于模式匹配,如果需要支持运行时变量,需采用传统判定。

8.3 避免滥用 var/discard#

  • 不要用 is var x 跳过 null 检查。
  • discard _ 只适合确实不关心值的场景,用后不要尝试访问。

8.4 List/Slice 模式性能#

使用 [..var arr, x] 这种 slice+变量捕获,编译器可能分配新数组,造成性能下降。大数据集合应谨慎。


9. 小结与展望#

Pattern Matching 是现代 C# 代码的“瑞士军刀”,能极大提升 if/else、switch/case 类代码的简洁性、表达力和类型安全性,在 switch 表达式等场景下优势更加明显。它不仅代码层面更美观,也能为某些条件分支提供更优指令级性能。然而,模式匹配并非多态(polymorphism)、虚方法的替代品!业务逻辑与类型分发仍建议用面向对象原则解决。

当前最大限制为:只能用常量做模式判定,变量(非编译期const)不支持,理由有 exhaustiveness 推导、避免副作用等。后续版本是否会提升,还需 C# 团队权衡设计复杂性。

未来,随着 C# 特性继续演进(如 pattern 泛型化、任意表达式匹配),我们有理由期待模式匹配变得更加强大和灵活。如果你还没用过模式匹配,是时候投入生产实践试试啦!

 

2025-06-09 11:32:51【出处】:https://www.cnblogs.com/InCerry/p/-/introduce-cs-pattern-match

=======================================================================================

posted on 2022-12-01 15:33  jack_Meng  阅读(941)  评论(0)    收藏  举报

导航