模式匹配

参考:https://www.cnblogs.com/markkang/p/14130867.html

 

什么是模式匹配?

在特定的上下文中,模式匹配是用于检查所给对象及属性是否满足所需模式(即是否符合一定标准)并从输入中提取信息的行为。它是一种新的代码流程控方式,它能使代码流可读性更强。这里说到的标准有“是不是指定类型的实例”、“是不是为空”、“是否与给定值相等”、“实例的属性的值是否在指定范围内”等。

模式匹配常结合is表达式用在if语句中,也可用在switch语句在switch表达式中,并且可以用when语句来给模式指定附加的过滤条件。它非常善于用来探测复杂对象,例如:外部Api返回的对象在不同情况下返回的类型不一致,如何确定对象类型?


C# 在 C# 7.0 中引入了模式匹配。 自此之后,每个主要 C# 版本都扩展了模式匹配功能。 以下 C# 表达式和语句
支持模式匹配:

is 表达式
switch 语句
switch 表达式(在 C# 8.0 中引入)

 

 模式匹配种类

在这些构造中,可将输入表达式与以下任一模式进行匹配:
声明模式:用于检查表达式的运行时类型,如果匹配成功,则将表达式结果分配给声明的变量。 在 C# 7.0 中引
入。

1、声明模式declaration pattern:用于检查表达式的运行时类型,如果匹配成功,则将表达式结果分配给声明的变量。 在 C# 7.0 中引
入。
2、类型模式type pattern:用于检查表达式的运行时类型。 在 C# 9.0 中引入。
3、常量模式constant pattern:用于测试表达式结果是否等于指定常量。 在 C# 7.0 中引入。
4、关系模式relational pattern:用于将表达式结果与指定常量进行比较。 在 C# 9.0 中引入。
5、属性模式property pattern:用于测试表达式的属性或字段是否与嵌套模式匹配。 在 C# 8.0 中引入。
6、位置模式positional pattern:用于解构表达式结果并测试结果值是否与嵌套模式匹配。 在 C# 8.0 中引入。
7、var 模式var pattern:用于匹配任何表达式并将其结果分配给声明的变量。 在 C# 7.0 中引入。
8、弃元模式discard pattern:用于匹配任何表达式。 在 C# 8.0 中引入。
9、逻辑模式logical pattern:用于测试表达式是否与模式的逻辑组合匹配。 在 C# 9.0 中引入。
     否定模式(C#9.0)
     合取模式(C#9.0)
     析取模式(C#9.0)
10、括号模式(C#9.0)
11、列表模式(C#11 list pattern)
12、切片模式(C#11 slice pattern)


 

后面内容,我们就以上这些模式以下面几个类型为基础进行写示例进行说明。

类型模式

类型模式检查表达式的运行时类型是否与给定类型兼容

object greeting = "Hello, World!";
if (greeting is string)
{
Console.WriteLine(“声明模式”); // output: hello, world!
}

声明模式

 借助声明模式,还可声明新的局部变量。
当声明模式与表达式匹配时,将为该变量分配转换后的表达式结果,如以下示例所示:

object greeting = "Hello, World!";
if (greeting is string message)
{
Console.WriteLine(message.ToLower()); // output: hello, world!
}

 

常量模式

从 C# 7.0 开始,可使用常量模式来测试表达式结果是否等于指定的常量,如以下示例所示:

public static decimal GetGroupTicketPrice(int visitorCount) => visitorCount switch
{
1 => 12.0m,
2 => 20.0m,
3 => 27.0m,
4 => 32.0m,
0 => 0.0m,
_ => throw new ArgumentException($"Not supported number of visitors: {visitorCount}",
nameof(visitorCount)),
};

在常量模式中,可使用任何常量表达式,例如:
integer 或 floating-point 数值文本
char 或 string 文本
布尔值 true 或 false
enum 值
声明常量字段或本地的名称
null
常量模式用于检查 null ,如以下示例所示:

if (input is null)
{
return;
}

编译器保证在计算表达式 x is null 时,不会调用用户重载的相等运算符 == 。
从 C# 9.0 开始,可使用否定 null 常量模式来检查非 NULL ,如以下示例所示:

if (input is not null)
{
// ...
}

有关详细信息,请参阅功能建议说明的常量模式部分。

 

关系模式

 从 C# 9.0 开始,可使用关系模式将表达式结果与常量进行比较,在关系模式中,

可使用关系运算符 < 、 > 、 <= 或 >= 中的任何一个。 关系模式的右侧部分必须是常数表达式。
常数表达式可以是 integer 、 floating-point、char 或 enum 类型。

Console.WriteLine(Classify(13)); // output: Too high
Console.WriteLine(Classify(double.NaN)); // output: Unknown
Console.WriteLine(Classify(2.4)); // output: Acceptable
static string Classify(double measurement) => measurement switch
{
< -4.0 => "Too low",
> 10.0 => "Too high",
double.NaN => "Unknown",
_ => "Acceptable",
};

要检查表达式结果是否在某个范围内,请将其与合取 and 模式匹配,如以下示例所示:

Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14))); // output: spring
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19))); // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17))); // output: winter
static string GetCalendarSeason(DateTime date) => date.Month switch
{
>= 3 and < 6 => "spring",
>= 6 and < 9 => "summer",
>= 9 and < 12 => "autumn",
12 or (>= 1 and < 3) => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};

如果表达式结果为 null 或未能通过可为空或取消装箱转换转换为常量类型,则关系模式与表达式不匹配。

 

逻辑模式

 ps:合取模式和析取模式对应心理学的 合取概念、析取概念。《普通心理学》第七章 思维,其中提到“概念” 的分类,分为合取概念、析取概念、关系概念。C#逻辑模式 就是对应概念的匹配。

从 C# 9.0 开始,可使用 not 、 and 和 or 模式连结符来创建以下逻辑模式:
否定 not 模式在否定模式与表达式不匹配时与表达式匹配。 下面的示例说明如何否定常量 null 模式来
检查表达式是否为非空值:

if (input is not null)
{
// ...
}


合取 and 模式在两个模式都与表达式匹配时与表达式匹配。 以下示例显示如何组合关系模式来检查值是
否在某个范围内:

Console.WriteLine(Classify(13)); // output: High
Console.WriteLine(Classify(-100)); // output: Too low
Console.WriteLine(Classify(5.7)); // output: Acceptable
static string Classify(double measurement) => measurement switch
{
< -40.0 => "Too low",
>= -40.0 and < 0 => "Low",
>= 0 and < 10.0 => "Acceptable",
>= 10.0 and < 20.0 => "High",
>= 20.0 => "Too high",
double.NaN => "Unknown",
};

 


析取 or 模式在任一模式与表达式匹配时与表达式匹配,如以下示例所示:

Console.WriteLine(GetCalendarSeason(new DateTime(2021, 1, 19))); // output: winter
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 10, 9))); // output: autumn
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 5, 11))); // output: spring
static string GetCalendarSeason(DateTime date) => date.Month switch
{
3 or 4 or 5 => "spring",
6 or 7 or 8 => "summer",
9 or 10 or 11 => "autumn",
12 or 1 or 2 => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month:
{date.Month}."),
};


如前面的示例所示,可在模式中重复使用模式连结符。
and 模式连结符的优先级高于 or 。 要显式指定优先级,请使用括号,如以下示例所示:

static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

 

属性模式

 从 C# 8.0 开始,可使用属性模式来将表达式的属性或字段与嵌套模式进行匹配,如以下示例所示:
static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };
当表达式结果为非 NULL 且每个嵌套模式都与表达式结果的相应属性或字段匹配时,属性模式将与表达式匹配。
还可将运行时类型检查和变量声明添加到属性模式,如以下示例所示:

Console.WriteLine(TakeFive("Hello, world!")); // output: Hello
Console.WriteLine(TakeFive("Hi!")); // output: Hi!
Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6', '7' })); // output: 12345
Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' })); // output: abc
static string TakeFive(object input) => input switch
{
string { Length: >= 5 } s => s.Substring(0, 5),
string s => s,
ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
ICollection<char> symbols => new string(symbols.ToArray()),
null => throw new ArgumentNullException(nameof(input)),
_ => throw new ArgumentException("Not supported input type."),
};

 


属性模式是一种递归模式。 也就是说,可以将任何模式用作嵌套模式。 使用属性模式将部分数据与嵌套模式进
行匹配,如以下示例所示:

 

public record Point(int X, int Y);
public record Segment(Point Start, Point End);
static bool IsAnyEndOnXAxis(Segment segment) =>
segment is { Start: { Y: 0 } } or { End: { Y: 0 } };

 
前面的示例使用 C# 9.0 及更高版本中提供的两个功能: or 模式连结符和记录类型。有关详细信息,请参阅功能建议说明的属性模式部分。

 

 

位置模式

位置模式是指通过添加解构函数将类型对象的属性解构成以元组方式组织的离散型变量,以便你可以使用这些属性作为一个模式进行检查。

由于位置型record类型、键值对 KeyValuePair<T,U>,默认已经带有解构函数Deconstruct,因此可以直接使用位置模式。如果是class和struct类型,则需要自己添加解构函数Deconstruct。我们也可以用扩展方法给一些类型添加解构函数Deconstruct。

例如我们给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})");
}

用在switch语句中例子如下:

static string Classify(Point point) => point switch
{
(0, 0) => "Origin",
(1, 0) => "positive X basis end",
(0, 1) => "positive Y basis end",
_ => "Just a point",
};

在前面的示例中,表达式的类型包含 Deconstruct 方法,该方法用于解构表达式结果。 还可将元组类型的表达式
与位置模式进行匹配。 这样,就可将多个输入与各种模式进行匹配,如以下示例所示:

static decimal GetGroupTicketPriceDiscount(int groupSize, DateTime visitDate)
=> (groupSize, visitDate.DayOfWeek) switch
{
(<= 0, _) => throw new ArgumentException("Group size must be positive."),
(_, DayOfWeek.Saturday or DayOfWeek.Sunday) => 0.0m,
(>= 5 and < 10, DayOfWeek.Monday) => 20.0m,
(>= 10, DayOfWeek.Monday) => 30.0m,
(>= 5 and < 10, _) => 12.0m,
(>= 10, _) => 15.0m,
_ => 0.0m,
};

 可在位置模式中使用元组元素的名称和 Deconstruct 参数,如以下示例所示:

var numbers = new List<int> { 1, 2, 3 };
if (SumAndCount(numbers) is (Sum: var sum, Count: > 0))
{
Console.WriteLine($"Sum of [{string.Join(" ", numbers)}] is {sum}"); // output: Sum of [1 2 3] is 6
}
static (double Sum, int Count) SumAndCount(IEnumerable<int> numbers)
{
int sum = 0;
int count = 0;
foreach (int number in numbers)
{
sum += number;
count++;
}
return (sum, count);
}


还可通过以下任一方式扩展位置模式:
添加运行时类型检查和变量声明,如以下示例所示:

public record Point2D(int X, int Y);
public record Point3D(int X, int Y, int Z);
static string PrintIfAllCoordinatesArePositive(object point) => point switch
{
Point2D (> 0, > 0) p => p.ToString(),//因为发生强制转换所以需要要写Point2D,object-》Point2D
Point3D (> 0, > 0, > 0) p => p.ToString(),//因为发生强制转换所以需要要写Point3D,object-》Point2D
_ => string.Empty,
};

前面的示例使用隐式提供 Deconstruct 方法的位置记录。
在位置模式中使用属性模式,如以下示例所示:

public record WeightedPoint(int X, int Y)
{
public double Weight { get; set; }
}
static bool IsInDomain(WeightedPoint point) => point is (>= 0, >= 0) { Weight: >= 0.0 };

结合前面的两种用法,如以下示例所示:

if (input is WeightedPoint (> 0, > 0) { Weight: > 0.0 } p)
{
// ..
}

位置模式是一种递归模式。 也就是说,可以将任何模式用作嵌套模式。
有关详细信息,请参阅功能建议说明的位置模式部分。
位模式和属性联合发复杂用法

    }
    static Point Transform(Point point) => point switch
    {
        (>=0,>4){ X: 1, Y: 0 } => new Point(0, 0),//这个的意思是x>0,y>4;属性X=1>,属性Y=0
        ( >= 3, >1) { X: var x, Y: var y } when x >y => new Point(3, 1),//这个的意思是x>3,y>1;属性X>属性Y
        ( >= 3, < 3) { X: >=0, Y: var y }  => new Point(3, 1),//这个的意思是x>3,y<3;属性X>0
        { X:  var x, Y: var y } when x < y => new Point(x + y, y),
        { X: var x, Y: var y } when x > y => new Point(x - y, y),
        { X: var x, Y: var y } => new Point(2 * x, 2 * y),
    };

 

 

 var 模式

从 C# 7.0 开始,可使用 var 模式来匹配任何表达式(包括 null ),并将其结果分配给新的局部变量,如以下示
例所示:

static bool IsAcceptable(int id, int absLimit) =>
SimulateDataFetch(id) is var results
&& results.Min() >= -absLimit
&& results.Max() <= absLimit;
static int[] SimulateDataFetch(int id)
{
var rand = new Random();
return Enumerable
.Range(start: 0, count: 5)
.Select(s => rand.Next(minValue: -10, maxValue: 11))
.ToArray();
}

 


需要布尔表达式中的临时变量来保存中间计算的结果时, var 模式很有用。 当需要在 switch 表达式或语句的
when 大小写临界子句中执行其他检查时,也可使用 var 模式,如以下示例所示:

public record Point(int X, int Y);
static Point Transform(Point point) => point switch
{
var (x, y) when x < y => new Point(-x, y),
var (x, y) when x > y => new Point(x, -y),
var (x, y) => new Point(x, y),
};
static void TestTransform()
{
Console.WriteLine(Transform(new Point(1, 2))); // output: Point { X = -1, Y = 2 }
Console.WriteLine(Transform(new Point(5, 2))); // output: Point { X = 5, Y = -2 }
}

 


在前面的示例中,模式 var (x, y) 等效于位置模式 (var x, var y) 。
在 var 模式中,声明变量的类型是与该模式匹配的表达式的编译时类型。
有关详细信息,请参阅功能建议说明的 Var 模式部分。
弃元模式
从 C# 8.0 开始,可使用弃元模式 _ 来匹配任何表达式,包括 null ,如以下示例所示:

Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday)); // output: 5.0
Console.WriteLine(GetDiscountInPercent(null)); // output: 0.0
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10)); // output: 0.0
static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch
{
DayOfWeek.Monday => 0.5m,
DayOfWeek.Tuesday => 12.5m,
DayOfWeek.Wednesday => 7.5m,
DayOfWeek.Thursday => 12.5m,
DayOfWeek.Friday => 5.0m,
DayOfWeek.Saturday => 2.5m,
DayOfWeek.Sunday => 2.0m,
_ => 0.0m,
};

 

在前面的示例中,弃元模式用于处理 null 以及没有相应的 DayOfWeek 枚举成员的任何整数值。 这可保证示例
中的 switch 表达式可处理所有可能的输入值。 如果没有在 switch 表达式中使用弃元模式,并且该表达式的任
何模式均与输入不匹配,则运行时会引发异常。 如果 switch 表达式未处理所有可能的输入值,则编译器会生成
警告。
弃元模式不能是 is 表达式或 switch 语句中的模式。 在这些案例中,要匹配任何表达式,请使用带有弃元
var _ 的 var 模式。

带括号模式
从 C# 9.0 开始,可在任何模式两边加上括号。

有了以上各种模式及其组合后,就牵扯到一个模式执行优先级顺序的问题,括号模式就是用来改变模式优先级顺序的,这与我们表达式中括号的使用是一样的。

如以下示例

if (input is not (float or double))
{
return;
}

C# 10

 扩展属性模式
列表模式
分片模式



C#11 模式匹配请看: https://www.cnblogs.com/hez2010/p/a-complete-guide-for-pattern-matching-in-csharp.html

posted @ 2021-10-01 15:43  小林野夫  阅读(703)  评论(0编辑  收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/