自学内容网 自学内容网

万字长文说说C#和Rust_Cangjie们的模式匹配

C#11新增列表模式后,这个从C#6就开始的特性,算是拼接好了模式匹配的最后一块拼图。对比Swift、Rust或Cangjie这类先天支持模式匹配的语言,能否一战?今天就来全面battle一下。Swift、Rust和Cangjie的模式匹配,一肪相承,这次对比,赶个热度,选取Cangjie。(Swift不熟悉,略懂Rust,据说Rust的模式匹配是抄Swift的?)

一、先说结论

  1. 模式匹配用于判断表达式是否符合某种特征,相对于普通的常量比较,它的比较范围更加丰富,比如可以比较类型。C#中,模式匹配可以用于switch表达式、switch语句、is表达式(常用于if语句);Cangjie中,模式匹配可用于match表达式、if-let表达式和if-while表达式。
  2. C#的模式匹配已经相当完整,覆盖了类型(类型模式)、基本类型(常量模式)、对象类型(属性模式)、元组类型(位置模式)、列表类型(列表模式),同时实现了通配符(弃元)、pattern guard(when)、表达式的捕获(var匹配任意表达式)、关系和逻辑运算等。打补丁走到强过天生模式匹配,相当哇塞、相当逆天了!!!
  3. Cangjie的模式匹配,目前应该还算是一个半成品,比如以下功能“似乎”还没有实现:对象类型匹配、列表类型匹配、关系和复杂逻辑运算等。

二、前置知识点

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,用于实现可空(第一次在Rust中看见这种可空实现,还是很震惊的)。可空变量的值,无论是空值还是有值,都被Option类型的枚举值包裹,需要通过模式匹配取出,所以枚举类型和模式匹配的关联性很强。当然,匹配枚举类型只是模式匹配的应用之一。
对于Cangjie的枚举,多说两句。它和Rust一样,是代数数据类型,但又阉割了一些功能,比如命名属性。和Rust一样,使用Option实现了可空类型,但异常又不像Rust一样使用Result<T, E>,而是使用传统的throw和try…catch…finally。无法评价优劣,但撕裂感是比较强烈的。

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<T>枚举==============================================================
//Option<T>枚举由框架提供,通过Option<T>实现框架的可空
//定义如下:
enum Option<T> {
    | Some(T) //有值构造器,其中T为值的类型
    | None //空值构造器
}
//使用Option<T>枚举
let a: Option<Int64> = Some(100) //Option<Int64>类型,Some(..)直接使用构造器创建实例
let b: ?Int64 = Some(100) //【?Int64】是Option<Int64>的简写,这就接近C#的可空表达了
let c: Option<String> = Some("Hello")
let d: ?String = None

let b: ?Int64 = 100 //这个写法不会报错,编译器会使用Some包装。所以b==100,结果是false
let a = None<Int64> //等价于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<Int64>.Some(2023)

    if (let Some(value) <- result) {
        println("操作成功,返回值为:${value}")
    } else {
        println("操作失败")
    }
}

//whilt-let和if-let差不多,只是while通过条件判断来循环执行语句
func recv(): Option<UInt8> {
    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<char> { 'a', 'b', 'c', 'd' };
Console.WriteLine(GetSourceLabel(letters));  // 2

static int GetSourceLabel<T>(IEnumerable<T> source) => source switch
{
    //array和collection类似is表达式中的尾随局部变量
    Array array => 1, //int[]是Array的子类
    ICollection<T> collection => 2, //List<char>是ICollection<T>的实现类
    _ => 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<char>(表达式值是其实现类)
    ICollection<char> symbols => new string(symbols.ToArray()),
    //匹配类型ICollection<char>,且Count属笥即元素个数>=5
    ICollection<char> { 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<int> { 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<int> 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<int> 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#的常量模式)

//不能使用关系运算和复杂的逻辑运算
//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)

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)
}

原文地址:https://blog.csdn.net/2401_85195613/article/details/140713787

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!