上一期中,我们学习了函数,以及数据是如何在管道中流动的。
在这一其中,我们将学习如何分解我的数据,用各种方式判断或者获取其中的信息。便是我们今天要学习的内容,模式匹配。
模式是转换输入数据的规则,在F#中,我们有非常多的模式支持,以供我们使用,本期内容我们就来介绍一下这些模式。以及一些模式匹配令人震惊的案例
| 名字 | 描述 | 例 |
|---|---|---|
| 常量模式 | 任何数值、字符或字符串文本、枚举常量或定义的文本标识符 | 1.0 、 |
| 标识符模式 | 可区分联合、异常标签或活动模式用例的 case 值 | Some(x)
|
| 变量模式 | identifier | a |
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 expr | nameof 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的炼丹炉



被折叠的 条评论
为什么被折叠?



