模式匹配

模式是转换输入数据的规则。 它们在整个 F# 中用于将数据与逻辑结构或结构进行比较,将数据分解为构成部分,或以各种方式从数据中提取信息。

言论

模式用于多种语言构造,例如 match 表达式。 在处理 let 绑定、lambda 表达式和与 try...with 表达式关联的异常处理程序中的函数的参数时,将使用它们。 有关详细信息,请参阅 Match 表达式let 绑定Lambda 表达式:fun 关键字异常:try...with 表达式

例如,在 match 表达式中,是管道符号后面的 模式。

match expression with
| pattern [ when condition ] -> result-expression
...

每个模式都充当以某种方式转换输入的规则。 在 match 表达式中,将依次检查每个模式,以查看输入数据是否与模式兼容。 如果找到匹配项,则执行结果表达式。 如果未找到匹配项,则会测试下一个模式规则。 Match 表达式中介绍了可选的 when condition 部分。

下表显示了支持的模式。 在运行时,系统按照表中列出的顺序针对以下每个模式测试输入,并以递归方式应用模式,即,从出现在代码中的第一个到最后一个,每行上的模式按从左到右的顺序。

名字 描述
常量模式 任何数值、字符或字符串文本、枚举常量或定义的文本标识符 1.0"test"30Color.Red
标识符模式 可区分联合、异常标签或活动模式用例的 case 值 Some(x)

Failure(msg)
变量模式 identifier a
as 模式 模式 作为 标识符 (a, b) as tuple1
OR 模式 pattern1 | pattern2 ([h] | [h; _])
AND 模式 pattern1pattern2 (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_npattern_n } { Name = name; }
通配符模式 _ _
模式和类型注释 模式类型 a : int
类型测试模式 :? type [ as identifier ] :? System.DateTime as dt
Null 模式 null null
Nameof 模式 nameof expr nameof str

常量模式

常量模式是数值、字符和字符串文本、枚举常量(包含枚举类型名称)。 只有常量模式的 match 表达式可以与其他语言中的 case 语句进行比较。 输入与文本值进行比较,如果值相等,则模式匹配。 字面量的类型必须与输入的类型兼容。

以下示例演示了文本模式的使用,还使用变量模式和 OR 模式。

[<Literal>]
let Three = 3

let filter123 x =
    match x with
    // The following line contains literal patterns combined with an OR pattern.
    | 1 | 2 | Three -> printfn "Found 1, 2, or 3!"
    // The following line contains a variable pattern.
    | var1 -> printfn "%d" var1

for x in 1..10 do filter123 x

文本模式的另一个示例是基于枚举常量的模式。 使用枚举常量时,必须指定枚举类型名称。

type Color =
    | Red = 0
    | Green = 1
    | Blue = 2

let printColorName (color:Color) =
    match color with
    | Color.Red -> printfn "Red"
    | Color.Green -> printfn "Green"
    | Color.Blue -> printfn "Blue"
    | _ -> ()

printColorName Color.Red
printColorName Color.Green
printColorName Color.Blue

标识符模式

如果模式是构成有效标识符的字符字符串,标识符的形式将确定模式的匹配方式。 如果标识符长于单个字符并且以大写字符开头,编译器将尝试与标识符模式匹配。 此模式的标识符可以是用 Literal 属性标记的值、可区分联合用例、异常标识符或活动模式用例。 如果未找到匹配标识符,匹配将失败,并且下一个模式规则(变量模式)与输入进行比较。

可区分联合模式可以是简单的命名用例,也可以有一个值,或者一个包含多个值的元组。 如果有值,则必须指定该值的标识符。 对于元组,必须为元组的每个元素提供一个带有标识符的元组模式,或者为一个或多个命名联合字段提供带字段名称的标识符。 有关示例,请参阅本节中的代码示例。

option 类型是具有两个用例(SomeNone)的可区分联合。 一个事例(Some)有一个值,但另一个(None)只是一个命名的事例。 因此,Some 需要具有与 Some 情况关联的值的变量,但 None 必须单独出现。 在以下代码中,变量 var1 给定通过匹配 Some 事例获取的值。

let printOption (data : int option) =
    match data with
    | Some var1  -> printfn "%d" var1
    | None -> ()

在以下示例中,PersonName 可区分联合包含表示可能的名称形式的字符串和字符组合。 可区分联合的用例为 FirstOnlyLastOnlyFirstLast

type PersonName =
    | FirstOnly of string
    | LastOnly of string
    | FirstLast of string * string

let constructQuery personName =
    match personName with
    | FirstOnly(firstName) -> printf "May I call you %s?" firstName
    | LastOnly(lastName) -> printf "Are you Mr. or Ms. %s?" lastName
    | FirstLast(firstName, lastName) -> printf "Are you %s %s?" firstName lastName

对于具有命名字段的可区分联合,可以使用等号 (=) 来提取命名字段的值。 以具有如下声明的可区分联合为例。

type Shape =
    | Rectangle of height : float * width : float
    | Circle of radius : float

可以在模式匹配表达式中使用命名字段,如下所示。

let matchShape shape =
    match shape with
    | Rectangle(height = h) -> printfn $"Rectangle with length %f{h}"
    | Circle(r) -> printfn $"Circle with radius %f{r}"

命名字段的使用是可选的,因此在前面的示例中,Circle(r)Circle(radius = r) 具有相同的效果。

指定多个字段时,请使用分号(;)作为分隔符)。

match shape with
| Rectangle(height = h; width = w) -> printfn $"Rectangle with height %f{h} and width %f{w}"
| _ -> ()

使用活动模式可以定义更复杂的自定义模式匹配。 有关活动模式的详细信息,请参阅 活动模式

标识符是异常的情况用于异常处理程序上下文中的模式匹配。 有关异常处理中的模式匹配的信息,请参阅 异常:try...with 表达式

变量模式

变量模式将匹配的值分配给变量名称,然后可用于 -> 符号右侧的执行表达式。 变量模式单独匹配任何输入,但变量模式通常出现在其他模式中,因此使更复杂的结构(如元组和数组)可以分解为变量。

以下示例演示元组模式中的变量模式。

let function1 x =
    match x with
    | (var1, var2) when var1 > var2 -> printfn "%d is greater than %d" var1 var2
    | (var1, var2) when var1 < var2 -> printfn "%d is less than %d" var1 var2
    | (var1, var2) -> printfn "%d equals %d" var1 var2

function1 (1,2)
function1 (2, 1)
function1 (0, 0)

as 模式

as 模式是一种包含 as 子句的模式。 as 子句将匹配的值绑定到一个名称,该名称可以在 match 表达式的执行过程中使用;或者,如果在 let 绑定中使用此模式,该名称将作为绑定项添加到本地作用域中。

以下示例使用 as 模式。

let (var1, var2) as tuple1 = (1, 2)
printfn "%d %d %A" var1 var2 tuple1

OR 模式

当输入数据可以匹配多个模式时,将使用 OR 模式,并且你想要执行与结果相同的代码。 OR 模式的两侧类型必须兼容。

以下示例演示 OR 模式。

let detectZeroOR point =
    match point with
    | (0, 0) | (0, _) | (_, 0) -> printfn "Zero found."
    | _ -> printfn "Both nonzero."
detectZeroOR (0, 0)
detectZeroOR (1, 0)
detectZeroOR (0, 10)
detectZeroOR (10, 15)

AND 模式

AND 模式要求输入匹配两种模式。 AND 模式的两侧类型必须兼容。

以下示例类似于本主题后面的元组模式部分中所示的 detectZeroTuple,但此处 var1var2 均使用 AND 模式获取为值。

let detectZeroAND point =
    match point with
    | (0, 0) -> printfn "Both values zero."
    | (var1, var2) & (0, _) -> printfn "First value is 0 in (%d, %d)" var1 var2
    | (var1, var2)  & (_, 0) -> printfn "Second value is 0 in (%d, %d)" var1 var2
    | _ -> printfn "Both nonzero."
detectZeroAND (0, 0)
detectZeroAND (1, 0)
detectZeroAND (0, 10)
detectZeroAND (10, 15)

Cons 模式

cons 模式用于将列表分解为第一个元素、,以及包含其余元素的列表、的列表。

let list1 = [ 1; 2; 3; 4 ]

// This example uses a cons pattern and a list pattern.
let rec printList l =
    match l with
    | head :: tail -> printf "%d " head; printList tail
    | [] -> printfn ""

printList list1

还可以将多个缺点模式链接在一起,以匹配以特定元素序列开头的列表。

let charList = ['A'; 'B'; 'C'; 'D']

// This example demonstrates multiple cons patterns.
let matchChars xs =
    match xs with
    | 'A'::'B'::t -> printfn "starts with 'AB', rest: %A" t
    | 'A'::t -> printfn "starts with 'A', rest: %A" t
    | 'C'::'D'::t -> printfn "starts with 'CD', rest: %A" t
    | _ -> printfn "does not match"

matchChars charList
matchChars ['A'; 'X']
matchChars ['C'; 'D'; 'E']

列表模式

列表模式使列表能够分解为多个元素。 列表模式本身只能匹配特定数量的元素的列表。

// This example uses a list pattern.
let listLength list =
    match list with
    | [] -> 0
    | [ _ ] -> 1
    | [ _; _ ] -> 2
    | [ _; _; _ ] -> 3
    | _ -> List.length list

printfn "%d" (listLength [ 1 ])
printfn "%d" (listLength [ 1; 1 ])
printfn "%d" (listLength [ 1; 1; 1; ])
printfn "%d" (listLength [ ] )

数组模式

数组模式类似于列表模式,可用于分解特定长度的数组。

// This example uses array patterns.
let vectorLength vec =
    match vec with
    | [| var1 |] -> var1
    | [| var1; var2 |] -> sqrt (var1*var1 + var2*var2)
    | [| var1; var2; var3 |] -> sqrt (var1*var1 + var2*var2 + var3*var3)
    | _ -> failwith (sprintf "vectorLength called with an unsupported array size of %d." (vec.Length))

printfn "%f" (vectorLength [| 1. |])
printfn "%f" (vectorLength [| 1.; 1. |])
printfn "%f" (vectorLength [| 1.; 1.; 1.; |])
printfn "%f" (vectorLength [| |] )

带圆括号模式

括号可以围绕模式进行分组,以实现所需的关联性。 在以下示例中,括号用于控制 AND 模式与 cons 模式之间的关联性。

let countValues list value =
    let rec checkList list acc =
       match list with
       | (elem1 & head) :: tail when elem1 = value -> checkList tail (acc + 1)
       | head :: tail -> checkList tail acc
       | [] -> acc
    checkList list 0

let result = countValues [ for x in -10..10 -> x*x - 4 ] 0
printfn "%d" result

元组模式

元组模式匹配元组形式的输入,并使元组能够通过使用元组中每个位置的模式匹配变量分解为其构成元素。

以下示例演示元组模式,还使用文本模式、变量模式和通配符模式。

let detectZeroTuple point =
    match point with
    | (0, 0) -> printfn "Both values zero."
    | (0, var2) -> printfn "First value is 0 in (0, %d)" var2
    | (var1, 0) -> printfn "Second value is 0 in (%d, 0)" var1
    | _ -> printfn "Both nonzero."
detectZeroTuple (0, 0)
detectZeroTuple (1, 0)
detectZeroTuple (0, 10)
detectZeroTuple (10, 15)

记录模式

记录模式用于分解记录以提取字段的值。 该模式不必引用记录的所有字段;任何省略的字段只是不参与匹配,并且不会提取。

// This example uses a record pattern.

type MyRecord = { Name: string; ID: int }

let IsMatchByName record1 (name: string) =
    match record1 with
    | { MyRecord.Name = nameFound; MyRecord.ID = _; } when nameFound = name -> true
    | _ -> false

let recordX = { Name = "Parker"; ID = 10 }
let isMatched1 = IsMatchByName recordX "Parker"
let isMatched2 = IsMatchByName recordX "Hartono"

通配符模式

通配符模式由下划线(_)字符表示,与变量模式相似,可以匹配任何输入,不同之处在于输入被放弃而不是被赋值给变量。 通配符模式通常在其他模式中用作 -> 符号右侧表达式中不需要的值的占位符。 通配符模式也经常在模式列表的末尾使用,以匹配任何不匹配的输入。 本主题中的许多代码示例演示了通配符模式。 有关一个示例,请参阅前面的代码。

以下代码显示了对通配符模式的一些附加用法:

// Wildcard pattern matching "nothing" examples

// Example 1: Wildcard ignoring function parameters
let ignoreAllParams _ _ = "Ignores all input"

// Example 2: Wildcard in destructuring, ignoring elements
let getFirstOnly (first, _) = first

// Example 3: Using wildcard to ignore optional values
let handleEmpty opt =
    match opt with
    | Some _ -> "Has something"
    | None -> "Has nothing"

// Usage
printfn "%s" (ignoreAllParams 42 "test")
printfn "%d" (getFirstOnly (1, "ignored"))
printfn "%s" (handleEmpty None)

具有类型注释的模式

模式可以具有类型注释。 这些行为与其他类型批注类似,并像其他类型注释一样进行引导推理。 模式中的类型注释需要用圆括号括起来。

具有类型注释的模式使用语法 pattern : type ,并向类型检查器提供 编译时类型信息 。 这纯粹是一个静态类型注释,有助于进行类型推理 - 它不执行任何运行时类型检查或转换。 编译器在编译期间使用此信息来确定模式变量的类型。

以下代码显示了具有类型批注的模式:

let detect1 x =
    match x with
    | 1 -> printfn "Found a 1!"
    | (var1 : int) -> printfn "%d" var1
detect1 0
detect1 1

在此示例中, (var1 : int) 告知编译器 var1 的类型 int。 这是在编译时解析的,生成的代码将在整个 var1 匹配表达式中视为整数。 此模式将匹配任何整数值并将其绑定到 var1

主要特征:

  • 使用语法 pattern : type (带有单个冒号)。
  • 在编译时解析 - 向类型检查器提供类型信息。
  • 不执行运行时类型测试。
  • 用于类型推理并指导编译器。

类型测试模式

类型测试模式用于 在运行时将输入与类型匹配。 如果输入类型与模式中指定的类型匹配(或派生类型),则匹配成功。

类型测试模式使用语法 :? type 并执行 运行时类型检查,类似于 is C# 中的或 as 运算符。 此模式测试某个值在程序执行过程中是否为特定类型,使其在处理继承层次结构或接口实现时很有用。

以下示例演示类型测试模式:

open System.Windows.Forms

let RegisterControl(control:Control) =
    match control with
    | :? Button as button -> button.Text <- "Registered."
    | :? CheckBox as checkbox -> checkbox.Text <- "Registered."
    | _ -> ()

如果仅检查标识符是否为特定派生类型,则不需要模式的 as identifier 部分,如以下示例所示:

type A() = class end
type B() = inherit A()
type C() = inherit A()

let m (a: A) =
    match a with
    | :? B -> printfn "It's a B"
    | :? C -> printfn "It's a C"
    | _ -> ()

主要特征:

  • 使用语法 :? type:? type as identifier (带问号)。
  • 运行时 解析 - 在执行期间执行实际类型检查。
  • 测试某个值是否是特定类型的实例或其派生类型。
  • 通常用于继承层次结构和多态类型。
  • 类似于 C# 的 is 运算符或 as 运算符。

对比类型注释和类型测试模式

虽然这两种模式都涉及类型,但它们的作用非常不同:

功能 / 特点 类型批注模式 (pattern : type 类型测试模式 (:? type
语法 单个冒号: a : int 带问号的冒号: :? Button
解决时 编译时间 运行时
Purpose 参考线类型推理 测试实际值类型
用例 帮助编译器了解类型 检查继承层次结构中的运行时类型
C 中的等效项# 切换模式中的类型批注 isas 运算符

以下示例演示了差异:

// Type annotation pattern - compile time
let detect1 x =
    match x with
    | 1 -> printfn "Found a 1!"
    | (var1 : int) -> printfn "%d" var1
// The ': int' tells the compiler var1 is an int
// Everything is resolved at compile time

// Type test pattern - runtime
type A() = class end
type B() = inherit A()

let test (a: A) =
    match a with
    | :? B -> printfn "Runtime check: it's a B"
    | _ -> printfn "Runtime check: it's not a B"
// The ':? B' performs a runtime type check
// The actual type is tested during execution

Null 模式

当你使用允许 Null 值的类型时,Null 模式会匹配可能出现的 Null 值。 与 .NET Framework 代码互操作时,通常会使用 Null 模式。 例如,.NET API 的返回值可能是 match 表达式的输入。 可以根据返回值是否为 null 以及返回值的其他特征来控制程序流。 可以使用 null 模式来防止 null 值传播到程序的其余部分。

以下示例使用 null 模式和变量模式。

let ReadFromFile (reader : System.IO.StreamReader) =
    match reader.ReadLine() with
    | null -> printfn "\n"; false
    | line -> printfn "%s" line; true

let fs = System.IO.File.Open("..\..\Program.fs", System.IO.FileMode.Open)
let sr = new System.IO.StreamReader(fs)
while ReadFromFile(sr) = true do ()
sr.Close()

对于 F# 9 为 null 性功能,也建议使用 null 模式:

let len (str: string | null) =
    match str with
    | null -> -1
    | s -> s.Length

同样,可以使用与为 null 性相关的新的专用模式

let let str =       // str is inferred to be `string | null`
    match str with
    | Null -> -1
    | NonNull (s: string) -> s.Length

Nameof 模式

nameof 模式的值等于 nameof 关键字后面的表达式时,该 nameof 模式与字符串匹配。 当需要将字符串值与代码中的类型名称、区分联合事例或其他符号匹配时,此模式特别有用。 使用 nameof 提供编译时安全性,因为如果重命名符号,模式将自动使用新名称。

常见的用例是反序列化数据,其中字符串值表示类型或大小写名称:

type EventType =
    | OrderCreated
    | OrderShipped
    | OrderDelivered

let handleEvent eventName data =
    match eventName with
    | nameof OrderCreated -> printfn "Processing order creation: %s" data
    | nameof OrderShipped -> printfn "Processing order shipment: %s" data
    | nameof OrderDelivered -> printfn "Processing order delivery: %s" data
    | _ -> printfn "Unknown event type: %s" eventName

handleEvent "OrderCreated" "Order #123" // matches first case
handleEvent "OrderShipped" "Order #123" // matches second case

这种方法比使用字符串文本(如 "OrderCreated")更好,因为:

  • 如果重命名 OrderCreatedOrderPlaced该模式,则模式会自动更新。
  • 编译器确保符号存在,防止拼写错误。
  • 重构时,代码保持一致。

还可以与参数一起使用 nameof

let f (str: string) =
    match str with
    | nameof str -> "It's 'str'!"
    | _ -> "It is not 'str'!"

f "str" // matches
f "asdf" // does not match

有关可以取其名称的内容的信息,请参阅 nameof 运算符。

另请参阅