模式比對

模式是轉換輸入數據的規則。 它們會在整個 F# 中用來比較數據與邏輯結構、將數據分解成組成部分,或以各種方式從數據中擷取資訊。

備註

模式用於許多語言建構,例如 match 表示式。 當您在 let 系結、Lambda 表達式和與 try...with 表達式相關聯的異常處理器中處理函式的引數時,會使用這些引數。 如需詳細資訊,請參閱 比對表達式、let 綁定、Lambda 表達式:fun 關鍵詞和 例外:try...with 表達式。

例如,在 match 表達式中,模式 是管道符號之後的內容。

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

每個模式都會作為某種方式轉換輸入的規則。 在 match 表示式中,會接著檢查每個模式,以查看輸入數據是否與模式相容。 如果找到匹配項,結果表達式就會被執行。 如果找不到相符項目,則會測試下一個模式規則。 在 比對表示式中說明選擇性的 條件 部分。

下表顯示支援的模式。 在運行時間,輸入會根據數據表中所列的順序,針對下列每個模式進行測試,而且模式會以遞歸方式套用,從第一個套用到最後一個,因為它們出現在您的程式代碼中,然後從左到右針對每一行的模式套用。

名字 描述 例
常數模式 任何數值、字元或字串常值、列舉常數或定義的常值識別碼 1.0、"test"、30、Color.Red
標識碼模式 區分聯合的案例值、一個例外狀況標籤或一個作用中模式案例 Some(x)

Failure(msg)
變數模式 識別碼 a
as 模式 模式 為 識別子 (a, b) as tuple1
OR 模式 圖案1 | 圖案2 ([h] | [h; _])
AND 模式 pattern1 和 pattern2 (a, b) & (_, "test")
缺點模式 識別碼 :: 清單識別碼 h :: t
列表樣式 [ pattern_1; ... pattern_n ] [ a; b; c ]
陣列模式 [| pattern_1; ..; pattern_n |] [| a; b; c |]
括弧模式 (模式) ( a )
Tuple 模式 (pattern_1,...,pattern_n) ( a, b )
記錄模式 { identifier1 = pattern_1;... ; identifier_n = pattern_n } { Name = name; }
通配符模式 _ _
模式與類型批注 模式:類型 a : int
類型測試圖樣 :? 類型 [ 作為 識別子 ] :? 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 類型是一個區分聯集,包含兩種情況:Some 和 None。 一個案例(Some)有一個值,但另一個案例(None)只是一個具名案例。 因此,Some 必須有與 Some 案例相關聯的值變數,但 None 必須單獨出現。 在下列程式代碼中,變數 var1 會透過比對 Some 案例來取得的值。

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

在下列範例中,PersonName 歧視聯集包含字串和字元的混合,這些字串和字元代表可能的名稱形式。 區分聯合的案例為 FirstOnly、LastOnly和 FirstLast。

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 表示式。

變數模式

變數模式會將比對的值指派給變數名稱,然後可用於 -> 符號右邊的執行表達式。 變數模式只會符合任何輸入,但變數模式通常會出現在其他模式內,因此可讓元組和數位等更複雜的結構分解成變數。

下列範例示範 Tuple 模式內的變數模式。

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 子句會將匹配的值綁定到一個可在 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 模式的兩端類型必須相容。

下列範例就像本主題稍後的 Tuple 模式一節中顯示的 detectZeroTuple,但在這裡,var1 和 var2 都是使用 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 模式可用來將清單分解成第一個元素、頭,以及包含其餘元素的清單,尾。

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

Tuple 模式

元組模式會比對元組形式的輸入,並允許使用模式比對變數來將元組分解為其組成元素。

下列範例示範元組模式,並使用常值模式、變數模式和通配符模式。

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)會告知 類型為 int的編譯器var1。 這會在編譯階段解決,而產生的程式碼會在整個比對運算式中視為 var1 整數。 此模式將匹配任何整數值並將其綁定到 var1。

主要特性:

  • 使用語法 pattern : type (具有單一冒號)。
  • 在 編譯階段 解析 - 提供類型資訊給類型檢查器。
  • 不執行執行階段類型測試。
  • 用於類型推斷和引導編譯器。

類型測試圖樣

類型測試模式可用來 在執行階段將輸入與類型進行比對。 如果輸入類型符合模式中指定的型別(或 衍生型別),則比對會成功。

類型測試模式使用語法 :? type 並執行 執行階段類型檢查,類似 is 於 C# 中的 or 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 or :? type as identifier (帶有問號)。
  • 在 運行時間 解決 - 在執行期間執行實際類型檢查。
  • 測試值是否為特定類型或其衍生類型的實例。
  • 常用於繼承階層和多態類型。
  • 類似於 C# 的 is 運算子或 as 運算子。

對比類型註釋和類型測試模式

雖然這兩種模式都涉及類型,但它們的用途卻截然不同:

特徵 / 功能 類型註解模式 (pattern : type) 型式測試模式 (:? type)
Syntax 單冒號: a : int 帶問號的冒號: :? Button
解決時 編譯時間 執行時間
Purpose 指南類型推斷 測試值的實際類型
使用案例 幫助編譯器理解類型 檢查繼承階層中的執行時期類型
C 中的對等內容# 開關模式中的類型註釋 is 或 as 運算子

下列範例示範差異:

// 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 模式

空值模式會匹配您在使用允許空值的類型時可能出現的空值。 與 .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 nullability 功能也建議使用 Null 模式:

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

同樣地,您可以使用專門針對可空值的 模式:

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

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")更好,因為:

  • 如果您重新命名 OrderCreated 為 OrderPlaced,型樣會自動更新。
  • 編譯器確保符號存在,防止拼字錯誤。
  • 重構時,您的程式碼會保持一致。

您也可以搭配參數使用 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 運算符。

另請參閱