【F#入门】第五讲 模式匹配

上一期中,我们学习了函数,以及数据是如何在管道中流动的。

在这一其中,我们将学习如何分解我的数据,用各种方式判断或者获取其中的信息。便是我们今天要学习的内容,模式匹配。

模式是转换输入数据的规则,在F#中,我们有非常多的模式支持,以供我们使用,本期内容我们就来介绍一下这些模式。以及一些模式匹配令人震惊的案例

名字

描述

常量模式

任何数值、字符或字符串文本、枚举常量或定义的文本标识符

1.0

"test"30Color.Red

标识符模式

可区分联合、异常标签或活动模式用例的 case 值

Some(x)



Failure(msg)

变量模式

identifiera
as

模式

pattern

asidentifier

(a, b) as tuple1

OR 模式

pattern1

|pattern2

([h] | [h; _])

AND 模式

pattern1

&pattern2

(a, b) & (_, "test")

Cons 模式

标识符

::列表标识符

h :: t

列表模式

[ pattern_1; ... ; pattern_n]

[ a; b; c ]

数组模式

[| pattern_1; ..; pattern_n|]

[| a; b; c |]

带括号模式

( pattern)

( a )

元组模式

( pattern_1, ... , pattern_n)

( a, b )

记录模式

{identifier1=pattern_1; ... ;identifier_n=pattern_n}

{ Name = name; }

通配符模式

_

_

模式和类型注释

pattern

:type

a : int

类型测试模式

:?type[ asidentifier]

:? System.DateTime as dt

Null 模式

null

Nameof 模式

nameof exprnameof str

模式匹配使业务规则更清晰可见,代码更接近问题域的表述

模式匹配是递归算法的自然表达方式,这在函数式编程中尤为重要

以下match with之间采用多采用直接使用字面量的方式直接使用,实际中当然可以使用变量来输入

常量模式 OR/And模式 _模式

// 常量模式
[<Literal>]
let Three = 3
let res = 
    match 2 with
    | 1 | 2 -> "one or two" // OR模式 / AND模式
    | Three -> "three"
    | _ -> "other" // 弃元模式
printfn "%A" res

常量模式可以直接使用例如 1, 2等字面量,也可以使用带有Literal特性(否则会变为变量模式)的常量.用于匹配特定值的情况

OR/AND模式, 通过| 和 &来表示模式的条件, 1 | 2表示的就是1或者2时都可以成功匹配

_模式, 与C#弃元模式一样,用于匹配剩余所有情况

变量模式

let res1 = 
    match 5 with
    // | x -> x + 1 // 变量模式
    | x when x < 5 -> x + 1 // 变量模式
    | _ -> 0

用于捕获符合条件的变量, 通过when来限定条件, 例如代码中 当输入小于5时,才会被该模式匹配

标识符模式

let sData = Some 1
let res2 = 
    match sData with
    | Some x when x < 5 -> x + 1 // 变量模式
    | None -> 1
    | _ -> 0 // 弃元模式
printfn "%A" res2

在之前的内容中我们已经见过他好多次了, 在函数式编程中我们经常会用到他, 用于匹配形如可区分联合的值(本例中匹配Option的Some 与None)

元组模式

let tupleA = (2, 2)
let res3 = 
    match tupleA with
    | (1, x)  -> x + 1 // 变量模式
    | (_, x)  -> x - 1 // 变量模式
    | _ -> 0 // 弃元模式
printfn "%A" res3

用于匹配元组中的多个元素,可与变量模式, 标识符模式等其他模式一起使用

Cons模式

// Cons模式
let listA = [1; 2; 3]
let res4 = 
    match listA with
    | [] -> 0 // 空列表模式
    | head::tail -> head // Cons模式
printfn "%A" res4

Cons模式用于处理列表的两部分, head代表第一个元素, tail代表剩下的所有元素,同样的,这个模式在函数式编程中,和递归中非常常见.

列表模式, 数组模式

// 列表模式/数组模式
let listB = [1; 2; 3]
let res5 = 
    match listB with
    | [1; 2] -> 0 // 列表模式
    | [1; v; 3] -> v // 列表模式
    | _ -> 0 // 弃元模式
let arrayA = [|1; 2; 3|]
let res6 = 
    match arrayA with
    | [|1; 2|] -> 0 // 数组模式
    | [|1; v; 3|] -> v // 数组模式
    | _ -> 0 // 弃元模式

与C#的列表模式类似,不过功能上稍弱,  用于匹配列表/数组的元素模式, 也可以和其他模式混合使用

记录模式

type RecordA = 
    {
        X: int
        Z: int
        Y: RecordA option
    }
let recordA = {X = 1; Z = 2; Y = None}
let res7 = 
    match recordA with
    | {X = 1; Y = Some {X = 1} } -> 1 // 记录模式
    | {Z = 1 } -> 2 // 记录模式
    | _ -> 0 // 弃元模式
printfn "%A" res7

用于匹配记录数据特征的模式匹配,由于函数式编程偏向纯粹的数据表达,该模式也会有这很强大的作用, 例子中第一个模式表达的就是当记录X为1, 并且Y是有值的Record且X也为1, 则进入该模式.

类型模式

match box 1 with
| :? int as i -> printfn "int %d" i
| :? string as s -> printfn "string %s" s

比较偏向oop的模式, 仅了解即可, 与C#的类型模式类似

活动模式

let (|Even|Odd|) aa = if aa % 2 = 0 then Even aa else Odd aa
let res8 = 
    match 1 with
    | Even x -> x + 1 
    | Odd x -> x - 1 
    | _ -> 0 // 弃元模式

let (|RGB|) (color: System.Drawing.Color) = 
    (color.R, color.G, color.B)

    
let (|RGB|HSB|) (color: System.Drawing.Color) = 
    if true then RGB (color.R, color.G, color.B)
    else HSB (color.A, color.B, color.G)

let ChooseColor color = 
    match color with
    | RGB (r, g, b) -> printfn "RGB %d %d %d" r g b 
    | HSB (h, s, b) -> printfn "HSB %d %d %d" h s b 

较为复杂的一种模式使用, 文档甚至为此单开了一页

有点类似于动态的生成了可区分联合,在第一行中,我们可以将输入值,明确的区分为Even或者Odd, 后续可以通过标识符模式正常使用

后两种使用方法,可以将数据在match中直接匹配为我们所需要的形式.

还有一些更为复杂的参数化活动模式等,较为复杂,意义不是特别大,这里就不做详细介绍了,有兴趣也可以参考微软文档介绍。活动模式 - F# | Microsoft Learn

一个有趣的模式匹配运用

最后,我们来看一个模式匹配的较为复杂运用,红黑树的实现

/// 红黑树的颜色
type Color = 
    | Red
    | Black
/// 红黑树节点结构
type RBTree<'T when 'T : comparison> =
    | Empty                                             // 空树
    | Node of Color * RBTree<'T> * 'T * RBTree<'T>      // 颜色, 左子树, 值, 右子树
/// 平衡红黑树
/// 通过模式匹配处理四种不平衡的情况
let balance = function
    | Black, Node(Red, Node(Red, a, x, b), y, c), z, d  // 情况1: 左-左红色节点
    | Black, Node(Red, a, x, Node(Red, b, y, c)), z, d  // 情况2: 左-右红色节点
    | Black, a, x, Node(Red, Node(Red, b, y, c), z, d)  // 情况3: 右-左红色节点
    | Black, a, x, Node(Red, b, y, Node(Red, c, z, d)) -> // 情况4: 右-右红色节点
        Node(Red, Node(Black, a, x, b), y, Node(Black, c, z, d))
    | color, l, v, r -> Node(color, l, v, r)  // 不需要平衡的情况
/// 向红黑树中插入值
let rec insert value tree =
    // 内部插入函数
    let rec ins = function
        | Empty -> Node(Red, Empty, value, Empty)  // 插入到空节点,创建红色节点
        | Node(color, left, v, right) as node ->
            if value < v then 
                // 插入到左子树
                balance(color, ins left, v, right)
            elif value > v then 
                // 插入到右子树
                balance(color, left, v, ins right)
            else 
                // 值已存在,返回原节点
                node
    
    // 确保根节点为黑色
    match ins tree with
    | Node(_, left, v, right) -> Node(Black, left, v, right)
    | Empty -> Empty  // 插入后不应该为空,这种情况实际不会发生
/// 创建空树
let empty = Empty
// 随便插入一些值
let tree = 
    empty
    |> insert 5
    |> insert 3
    |> insert 7
    |> insert 1
    |> insert 4
    |> insert 6
    |> insert 8
    |> insert 2
    |> insert 9
    |> insert 0
    |> insert 10
    |> insert 11
    |> insert 12
let printTree tree =
    // Generated by Copilot
    let rec printNode prefix isLeft node =
        match node with
        | Empty -> printfn "%s--(empty)" prefix
        | Node(color, left, value, right) ->
            // 为节点颜色选择标记
            let colorMark = match color with
                            | Red -> "R"
                            | Black -> "B"
            
            // 打印当前节点值
            printfn "%s%s(%s)%A" prefix (if isLeft then "L--" else "R--") colorMark value
            
            // 计算子节点的缩进前缀
            let childPrefix = prefix + (if isLeft then "|   " else "    ")
            
            // 递归打印左右子树
            printNode childPrefix true left
            printNode childPrefix false right
    
    match tree with
    | Empty -> printfn "(empty tree)"
    | Node(color, left, value, right) ->
        // 为根节点颜色选择标记
        let colorMark = match color with
                        | Red -> "R"
                        | Black -> "B"
        
        // 打印根节点
        printfn "Root(%s)%A" colorMark value
        
        // 打印左右子树
        printNode "  " true left
        printNode "  " false right
// 测试打印函数
printTree tree

如果正常使用命令式的方式编写红黑树,我们可能需要较为复杂的状态判断,以及平衡调整等, 但如果我们使用函数式编程,并使用模式匹配去实现这一点,在刚开始接触时或许会看的有点云里雾里,  不过当我们了解模式匹配的本质后, 这段代码实际上非常清晰明了(虽然效率可能并不特别理想,但这是一种非常有趣的实现方式).

以一种模式作为例子

| Black, Node(Red, Node(Red, a, x, b), y, c), z, d 

我们再看到node的定义 

Node(color, left, value, right)

可以发现,这个模式代表着节点颜色为, 且左子树的节点颜色为,且左子树的左子树节点颜色为, 当满足这个条件时,就会进入这个模式. 其他部分的字母则是捕获的变量. 

这么一看,是不是逻辑其实非常的清晰呢?

其他的模式也都是一样的道理, 你能写出其代表的意义吗?

当然,如果变量名称写的更具体,可能会使其更具可读性. 模式匹配可以轻松的判断数据的特征, 状态,以及使用这些数据, 而不用去关心无聊的边界问题. 这些复杂繁琐的事情我们丢给编译器帮我们完成. 这是一种更高级的表述模式, 也意味着编译器可以对我们的代码进行更多的优化. 

模式匹配不止在函数式编程中非常有用, 利用好模式匹配, 能够获得非常简洁漂亮的代码!

Bilibili: @无聊的年 【F#入门】(五)模式匹配_哔哩哔哩_bilibili

微信公众号:@scixing的炼丹炉

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值