C#11新增列表模式后,这个从C#6就开始的特性,算是拼接好了模式匹配的最后一块拼图。对比Swift、Rust或Cangjie这类先天支持模式匹配的语言,能否一战?今天就来全面battle一下。Swift、Rust和Cangjie的模式匹配,一肪相承,这次对比,赶个热度,选取Cangjie。(Swift不熟悉,略懂Rust,据说Rust的模式匹配是抄Swift的?)
一、先说结论
- 模式匹配用于判断表达式是否符合某种特征,相对于普通的常量比较,它的比较范围更加丰富,比如可以比较类型。C#中,模式匹配可以用于switch表达式、switch语句、is表达式(常用于if语句);Cangjie中,模式匹配可用于match表达式、if-let表达式和if-while表达式。
- C#的模式匹配已经相当完整,覆盖了类型(类型模式)、基本类型(常量模式)、对象类型(属性模式)、元组类型(位置模式)、列表类型(列表模式),同时实现了通配符(弃元)、pattern guard(when)、表达式的捕获(var匹配任意表达式)、关系和逻辑运算等。打补丁走到强过天生模式匹配,相当哇塞、相当逆天了!!!
- Cangjie的模式匹配,目前应该还算是一个半成品,比如以下功能“似乎”还没有实现:对象类型匹配、列表类型匹配、关系(常量范围)和复杂逻辑运算等。相比于Rust,完成度还需要继续努力。
二、前置知识点
1.1 C#的switch表达式和is表达式
大多数情况下,我们都是使用switch和if语句,比较少接触switch和is表达式,尤其是switch表达式,好些人可能从来都没用过。
1.1.1 switch表达式
//使用方式1:函数式==============================================
class Program
{
static void Main(string[] args)
{
var score = 100;
Console.WriteLine(ReadScore(score));
}
//使用了常量匹配、逻辑匹配和关系匹配
//【=> 参数 switch】,将参数带入方法体
static string ReadScore(int num) => num switch
{
100 => "满分", //每个匹配逗号分隔
>=80 and <100 => "A",
>=60 and <80 => "B",
>=0 and <60 => "不及格",
_ => $"无效分{num}" //读取参数值
}; //分号结尾
}
//使用方式2:表达式==============================================
class Program
{
static void Main(string[] args)
{
var score = 100;
var result = score switch
{
100 => "满分",
>= 80 and < 100 => "A",
>= 60 and < 80 => "B",
>= 0 and < 60 => "不及格",
_ => $"无效分{score}"
}; //分号结尾
Console.WriteLine(result);
}
}
1.1.2 is表达式
//1、在if语句中使用==============================================
//判断是否为null,常量模式匹配-----------
if (input is null)
{
return;
}
//判断是否不为null------------------------------
if (result is not null)
{
Console.WriteLine(result.ToString());
}
//类型模式匹配----------------------------
int i = 34;
object iBoxed = i;
int? jNullable = 42;
//iBoxed表达式的值是否属于int类型,如果是,则将值赋值给变量a
//jNullable表达式的值是否属于int类型,如果是,则将值赋值给变量b
//注意并集条件用&&
if (iBoxed is int a && jNullable is int b)
{
Console.WriteLine(a + b); // 76
}
//和switch一样,也可以在方法中使用---------------
//如下is返回一个布尔值。而switch是分支选择。
//以下使用到了属性匹配,详见本文的模式匹配
static bool IsFirstFridayOfOctober(DateTime date) =>
date is { Month: 10, Day: <=7, DayOfWeek: DayOfWeek.Friday };
1.2 Cangjie的枚举类型
Cangjie们的枚举类型是代数数据类型,枚举值可以带参。框架内置了一个非常重要的泛型枚举Option
对于Cangjie的枚举,多说两句。它和Rust一样,是代数数据类型,但又阉割了一些功能,比如命名属性。和Rust一样,使用Option
1.2.1 枚举和内置枚举类型Option
//1、枚举====================================================
//Cangjie中枚举选项称为构造器
//构造器可以带参数,而且类似方法,可以重载
//在枚举中,还可以定义成员函数、操作符函数和成员属性,本例略
enum RGBColor {
| Red
| Green
| Blue
| Red(UInt8)
| Green(UInt8)
| Blue(UInt8)
}
//使用枚举
main() {
let r = RGBColor.Red //通过【类型名.构造器】创建枚举实例
let g = Green //如果没有Green命名冲突,可以直接使用构造器创建枚举实例
let b = Blue(100) //带参数的枚举实例,Blue(100)和Blut(101),是不同的值
}
//2、Option枚举=============================================
//Option枚举由框架提供,通过Option实现框架的可空
//定义如下:
enum Option {
| Some(T) //有值构造器,其中T为值的类型
| None //空值构造器
}
//使用Option枚举
let a: Option = Some(100) //Option类型,Some(..)直接使用构造器创建实例
let b: ?Int64 = Some(100) //【?Int64】是Option的简写,这就接近C#的可空表达了
let c: Option = Some("Hello")
let d: ?String = None
let b: ?Int64 = 100 //这个写法不会报错,编译器会使用Some包装。所以b==100,结果是false
let a = None //等价于let a: ?Int64=None。如果let a=None,报错,因为无法确定类型
1.2.2 通过match模式匹配获取Option的T值
//1、通过模式匹配获取T值==========================================
func getString(p: ?Int64): String{
match (p) {
case Some(x) => "${x}" //使用到了绑定匹配,取出x值
case None => "none"
}
}
main() {
let a = Some(1)
let b: ?Int64 = None
let r1 = getString(a)
let r2 = getString(b)
println(r1)
println(r2)
}
//2、当然,Cangjie也提供了解构T值的语法糖-getOrThrow方法
main() {
let a = Some(1)
let b: ?Int64 = None
let r1 = a.getOrThrow()
println(r1)
try {
let r2 = b.getOrThrow()
} catch (e: NoneValueException) {
println("b is None")
}
}
1.2.2 if-let和if-while(类似C#中的is)
//macth用于分支选择,if-let用于真假判断,也可用于提升Some(T)的T值
main() {
let result = Option.Some(2023)
if (let Some(value) <- result) {
println("操作成功,返回值为:${value}")
} else {
println("操作失败")
}
}
//whilt-let和if-let差不多,只是while通过条件判断来循环执行语句
func recv(): Option {
let number = Random().nextUInt8()
if (number < 128) {
return Some(number)
}
return None
}
main() {
//判断循环
while (let Some(data) <- recv()) {
println(data)
}
println("receive failed")
}
三、C#的模式匹配
2.1 类型模式
//1、匹配类型==================================================
/*验证表达式的运行时类型是否与给定类型兼容,兼容有三种情况
1)表达式类型是给定类型是的子类
2)表达式类型是给定类型是的实现类
3)或者存在从给定类型到表达式类型的隐式转化(如装箱拆箱)
*/
//下例中,object和string存在隐式转化------------------------
object greeting = "Hello, World!";
if (greeting is string message) //如果匹配,表达式结果将赋值给尾随局部变量message
{
Console.WriteLine(message.ToLower()); // output: hello, world!
}
//下例中,表达式类型分别是给定类型的子类或实现类-------------
//switch表达式用于分支选择,必须穷尽所有情况,最后一个分支通常使用_,类似default
var numbers = new int[] { 10, 20, 30 };
Console.WriteLine(GetSourceLabel(numbers)); // 1
var letters = new List { 'a', 'b', 'c', 'd' };
Console.WriteLine(GetSourceLabel(letters)); // 2
static int GetSourceLabel(IEnumerable source) => source switch
{
Array array => 1, //int[]是Array的子类
ICollection collection => 2, //List是ICollection的实现类
_ => 3, //通配符,匹配任何表达式
};
//2、弃元_,用于匹配任何表达式=======================================
//弃元除了用于switch的分支,还可以用于类型模式、位置模式和列表模式,类似占位符
//下例中,弃元用于匹配类型模式中的局部变量,以及通配符
public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
{
Car _ => 2.00m,
null => throw new ArgumentNullException(nameof(vehicle)),
_ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
};
2.2 常量模式
//匹配常量
/*验证表达式的值是否与给定常量匹配(包括相等、比较、逻辑等),适用于以下类型或值
1)数值、布尔、字符、字符串
2)enum值、const字段
3)null(见1.1.2节)
*/
//1、常量相等匹配===============================================
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($"不支持: {visitorCount}", nameof(visitorCount)),
};
//2、常量比较匹配===============================================
//<、>、<= 或 >= 中的任何一个
static string Classify(double measurement) => measurement switch
{
< -4.0 => "Too low",
> 10.0 => "Too high",
double.NaN => "Unknown",
_ => "Acceptable",
};
//3、逻辑匹配==================================================
//使用and or not (),匹配多种模式,除了用于常量匹配,也可用于其它匹配模式
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14))); // 春
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19))); // 夏
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17))); // 冬
static string GetCalendarSeason(DateTime date) => date.Month switch
{
>= 3 and < 6 => "春",
>= 6 and < 9 => "夏",
>= 9 and < 12 => "秋",
12 or (>= 1 and < 3) => "冬",
_ => throw new ArgumentException(nameof(date), $"找不到{date.Month}."),
};
2.3 属性模式
//1、属性模式的基本使用===========================================
//如果表达式的值是复杂类型,可以匹配表达式结果(对象实例)的属性值
//属性值可以当成常量,使用相等、比较、逻辑等方式匹配
//如下例中date是DateTime的实例,匹配属性Year为2020,Month为5...
static bool IsConferenceDay(DateTime date) =>
date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };
//2、属性模式和类型模式一起使用======================================
Console.WriteLine(TakeFive("Hello, world!")); // Hello
Console.WriteLine(TakeFive("Hi!")); // Hi!
Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6'})); // 12345
Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' })); // abc
static string TakeFive(object input) => input switch
{
//匹配类型string
string s => s,
//匹配类型string,且Length属性即字符长度>=5
string { Length: >= 5 } s => s.Substring(0, 5),
//匹配类型ICollection(表达式值是其实现类)
ICollection symbols => new string(symbols.ToArray()),
//匹配类型ICollection,且Count属笥即元素个数>=5
ICollection { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
//其它匹配情况
null => throw new ArgumentNullException(nameof(input)),
_ => throw new ArgumentException("Not supported input type."),
};
//3、属性模式的嵌套==============================================
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 } };
//重构一下
static bool IsAnyEndOnXAxis(Segment segment) =>
segment is { Start.Y: 0 } or { End.Y: 0 };
2.4 位置模式
//1、解构器==================================================
//1.1 什么是解构器-----------------------------------
//C# 中的 Deconstruct 方法(解构器)可以让实例能像元组一样被析构
//它是一种公开无返回值的方法,方法名必须为Deconstruct,且所有参数均为out参数
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y){ X = x; Y = y; }
//解构器,
public void Deconstruct(out int x, out int y)
{
x = X;
y = Y;
}
}
//使用解构器分解 Point 对象的成员
Point point = new Point(10, 20);
var (x, y) = point;
Console.WriteLine($"x: {x}, y: {y}");
//1.2 按位置匹配含有Deconstruct解构器的对象------------
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",
};
//2、多参数,也可以使用元组对参数进行包装,然后使用位置匹配==================
//每个位置,都可以使用常量相等、比较、逻辑、弃元、类型、属性等匹配模式
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,
};
//3、可以使用命名元组元素的名称,或者Deconstruct解构器的参数名称==============
//var用于捕获位置元素,并赋值给局部变量sum
var numbers = new List { 1, 2, 3 };
if (SumAndCount(numbers) is (Sum: var sum, Count: > 0))
{
Console.WriteLine($"Sum of [{string.Join(" ", numbers)}] is {sum}");
}
//根据给定整数列表,生成由(求合,求数)组成的命名元组
static (double Sum, int Count) SumAndCount(IEnumerable numbers)
{
int sum = 0;
int count = 0;
foreach (int number in numbers)
{
sum += number;
count++;
}
return (sum, count);
}
2.5 列表模式
//1、列表模式和位置模式比较像,位置模式用于元组,列表模式用于数组或列表==========
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers is [1, 2, 3]); // True
Console.WriteLine(numbers is [1, 2, 4]); // False
Console.WriteLine(numbers is [1, 2, 3, 4]); // False
Console.WriteLine(numbers is [0 or 1, <= 2, >= 3]); // True
//2、弃元_可以匹配任何表达式,var用于捕获位置元素,并赋值给局部变量=============
List numbers = new() { 1, 2, 3 };
if (numbers is [var first, _, _])
{
Console.WriteLine($"The first element of a three-item list is {first}.");
}
//3、切片模式,[..]最多只能使用一次====================================
Console.WriteLine(new[] { 1, 2, 3, 4, 5 } is [> 0, > 0, ..]); // True
Console.WriteLine(new[] { 1, 1 } is [_, _, ..]); // True
Console.WriteLine(new[] { 0, 1, 2, 3, 4 } is [> 0, > 0, ..]); // False
Console.WriteLine(new[] { 1 } is [1, 2, ..]); // False
Console.WriteLine(new[] { 1, 2, 3, 4 } is [.., > 0, > 0]); // True
Console.WriteLine(new[] { 2, 4 } is [.., > 0, 2, 4]); // False
Console.WriteLine(new[] { 2, 4 } is [.., 2, 4]); // True
Console.WriteLine(new[] { 1, 2, 3, 4 } is [>= 0, .., 2 or 4]); // True
Console.WriteLine(new[] { 1, 0, 0, 1 } is [1, 0, .., 0, 1]); // True
Console.WriteLine(new[] { 1, 0, 1 } is [1, 0, .., 0, 1]); // False
//4、列表模式其它模式一起使用=======================================
var result = numbers is [< 0, .. { Length: 2 or 4 }, > 0] ? "valid" : "not valid";
var result = message is ['a' or 'A', .. var s, 'a' or 'A']
? $"Message {message} matches; inner part is {s}."
: $"Message {message} doesn't match.";
2.6 var和when
//var用于匹配任何表达式(包括 null),并将其结果分配给新的局部变量(捕获)
//when用于对表达式进行进一步判断匹配
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))); // Point { X = -1, Y = 2 }
Console.WriteLine(Transform(new Point(5, 2))); // Point { X = 5, Y = -2 }
}
四、Cangjie的模式匹配
3.1 类型模式(C#的类型模式)
//父类
open class Base {
var a: Int64
public init() {
a = 10
}
}
//子类
class Derived <: Base {
public init() {
a = 20
}
}
//匹配类型
main() {
var d = Derived()
var r = match (d) {
case b: Base => b.a //匹配上,b可以是任意标识符,类似C#的尾随变量
case _ => 0
}
}
//如果不需要捕获变量,可以使用通配符
main() {
var d = Derived()
var r = match (d) {
case _: Base => 1 //匹配上
case _ => 0
}
}
3.2 常量模式(C#的常量模式)
//不能使用关系运算和复杂的逻辑运算,对比C#,比较弱
//Rust中可以使用区间0..10,但Cangjie目前还不支持
main() {
let score = 90
let level = match (score) {
case 0 | 10 | 20 | 30 | 40 | 50 => "D" //【|】表示或
case 60 => "C"
case 70 | 80 => "B"
case 90 | 100 => "A" // Matched.
case _ => "Not a valid score"
}
println(level)
}
3.3 绑定模式(类似C#的var)
//用于匹配任何表达式,并将其结果分配给新的局部变量(捕获)
//注意:如果模式使用了【|】,则不能使用绑定模式
main() {
let x = -10
let y = match (x) {
case 0 => "zero"
case n => "x is not zero and x = ${n}" // Matched.
}
println(y)
}
//上例在C#中的实现
class Program
{
static void Main(string[] args)
{
var x = -10;
var y = x switch
{
0 => "zero",
var n => $"x is not zero and x={n}"
};
Console.WriteLine(y);
}
}
3.4 Tuple模式(类似C#的位置模式)
//下例中,同时使用了绑定模式和通配符
main() {
let tv = ("Alice", 24)
let s = match (tv) {
case ("Bob", age) => "Bob is ${age} years old"
case ("Alice", age) => "Alice is ${age} years old" // Matched
case (name, 100) => "${name} is 100 years old"
case (_, _) => "someone"
}
println(s)
}
//注意,同一个Tuple中,不允许使用两个名称相同的绑定
case (x, x) => "someone" //报错
3.5 enum模式(C#中enum模式在常量模式中)
//在C#中,枚举是简单的常量值
//而在Cangjie中,枚举是代数数据类型,表现更加丰富
enum TimeUnit {
| Year(UInt64)
| Month(UInt64)
}
//通过模式匹配,解构T值
main() {
let x = Year(2)
let s = match (x) {
case Year(n) => "x has ${n * 12} months" // Matched,并解构T值
case TimeUnit.Month(n) => "x has ${n} months"
}
println(s)
}
//枚举模式的嵌套,元组模式也可以
enum TimeUnit {
| Year(UInt64)
| Month(UInt64)
}
enum Command {
| SetTimeUnit(TimeUnit)
| GetTimeUnit
| Quit
}
main() {
let command = SetTimeUnit(Year(2022))
match (command) {
case SetTimeUnit(Year(year)) => println("Set year ${year}")
case SetTimeUnit(Month(month)) => println("Set month ${month}")
case _ => ()
}
}
3.6 where(类似C#中的when,pattern guard)
enum RGBColor {
| Red(Int16) | Green(Int16) | Blue(Int16)
}
main() {
let c = RGBColor.Green(-100)
let cs = match (c) {
case Red(r) where r < 0 => "Red = 0"
case Red(r) => "Red = ${r}"
case Green(g) where g < 0 => "Green = 0" // Matched.
case Green(g) => "Green = ${g}"
case Blue(b) where b < 0 => "Blue = 0"
case Blue(b) => "Blue = ${b}"
}
print(cs)
}