檢視原始碼 模式和守衛
Elixir 提供模式配對,讓我們可以斷言資料結構的形狀或從中萃取值。模式通常會搭配守衛,讓開發人員可以執行較複雜的檢查,儘管有限。
本文件提供模式和守衛的完整參考,包括其語意、允許使用的地方,以及如何擴充它們。
模式
Elixir 中的模式由變數、文字和資料結構特定語法組成。用於執行模式配對的最常用結構之一是 match 運算子 (=
)
iex> x = 1
1
iex> 1 = x
1
在上面的範例中,x
起初沒有值,並將 1
指定給它。然後,我們將 x
的值與文字 1
進行比較,由於它們都是 1
,因此成功。
將 x
與 2 進行配對會引發
iex> 2 = x
** (MatchError) no match of right hand side value: 1
模式不是雙向的。如果你有一個從未指定值的變數 y
(通常稱為未繫結變數),而你寫下 1 = y
,將會引發錯誤
iex> 1 = y
** (CompileError) iex:2: undefined variable "y"
換句話說,模式只允許出現在 =
的左側。 =
的右側遵循語言的常規評估語意。
現在讓我們介紹每個結構的模式配對規則,然後介紹每個相關資料類型。
變數
模式中的變數總是指定給
iex> x = 1
1
iex> x = 2
2
iex> x
2
換句話說,Elixir 支援重新繫結。如果你不希望變數的值變更,可以使用 pin 運算子 (^
)
iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2
如果同一個變數在同一個模式中出現多次,則所有變數都必須繫結到同一個值
iex> {x, x} = {1, 1}
{1, 1}
iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}
底線變數 (_
) 具有特殊意義,因為它永遠無法繫結到任何值。當您不關心模式中的特定值時,它特別有用
iex> {_, integer} = {:not_important, 1}
{:not_important, 1}
iex> integer
1
iex> _
** (CompileError) iex:3: invalid use of _
文字 (數字和原子)
原子和數字 (整數和小數) 可以出現在模式中,它們總是按原樣表示。例如,原子只會與原子相符,如果它們是同一個原子
iex> :atom = :atom
:atom
iex> :atom = :another_atom
** (MatchError) no match of right hand side value: :another_atom
類似的規則適用於數字。最後,請注意模式中的數字執行嚴格比較。換句話說,整數與浮點數不相符
iex> 1 = 1.0
** (MatchError) no match of right hand side value: 1.0
元組
元組可以使用大括號語法 ({}
) 出現在模式中。模式中的元組只會與大小相同的元組相符,其中每個個別元組元素也必須相符
iex> {:ok, integer} = {:ok, 13}
{:ok, 13}
# won't match due to different size
iex> {:ok, integer} = {:ok, 11, 13}
** (MatchError) no match of right hand side value: {:ok, 11, 13}
# won't match due to mismatch on first element
iex> {:ok, binary} = {:error, :enoent}
** (MatchError) no match of right hand side value: {:error, :enoent}
清單
清單可以使用方括號語法 ([]
) 出現在模式中。模式中的清單只會與大小相同的清單相符,其中每個個別清單元素也必須相符
iex> [:ok, integer] = [:ok, 13]
[:ok, 13]
# won't match due to different size
iex> [:ok, integer] = [:ok, 11, 13]
** (MatchError) no match of right hand side value: [:ok, 11, 13]
# won't match due to mismatch on first element
iex> [:ok, binary] = [:error, :enoent]
** (MatchError) no match of right hand side value: [:error, :enoent]
與元組相反,清單還允許使用 [head | tail]
符號對非空清單進行匹配,該符號與清單的 head
和 tail
相符
iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]
多個元素可以作為 | tail
結構的前綴
iex> [first, second | tail] = [1, 2, 3]
[1, 2, 3]
iex> tail
[3]
注意 [head | tail]
不與空清單相符
iex> [head | tail] = []
** (MatchError) no match of right hand side value: []
由於字元清單表示為整數清單,因此也可以使用清單串接運算子 (++
) 對字元清單執行前綴匹配
iex> ~c"hello " ++ world = ~c"hello world"
~c"hello world"
iex> world
~c"world"
這等於與 [?h, ?e, ?l, ?l, ?o, ?\s | world]
相符。後綴匹配 (hello ++ ~c" world"
) 不是有效的模式。
地圖
地圖可能出現在使用百分比符號後接大括號語法的模式中 (%{}
)。與清單和元組相反,地圖執行子集比對。這表示地圖模式會比對任何包含模式中所有金鑰的其他地圖。
以下是一個所有金鑰都比對的範例
iex> %{name: name} = %{name: "meg"}
%{name: "meg"}
iex> name
"meg"
以下是一個只有子集金鑰比對的範例
iex> %{name: name} = %{name: "meg", age: 23}
%{age: 23, name: "meg"}
iex> name
"meg"
如果模式中的金鑰在地圖中不存在,則它們不會比對
iex> %{name: name, age: age} = %{name: "meg"}
** (MatchError) no match of right hand side value: %{name: "meg"}
請注意,空地圖會比對所有地圖,這與元組和清單形成對比,其中空元組或空清單只會分別比對空元組和空清單
iex> %{} = %{name: "meg"}
%{name: "meg"}
最後,請注意模式中的地圖金鑰必須永遠是文字或先前使用 pin 運算子比對的已繫結變數。
結構
結構可能出現在使用百分比符號、結構模組名稱或變數後接大括號語法的模式中 (%{}
)。
給定下列結構
defmodule User do
defstruct [:name]
end
以下是一個所有金鑰都比對的範例
iex> %User{name: name} = %User{name: "meg"}
%User{name: "meg"}
iex> name
"meg"
如果給定未知金鑰,編譯器會產生錯誤
iex> %User{type: type} = %User{name: "meg"}
** (CompileError) iex: unknown key :type for struct User
當放入變數而不是模組名稱時,可以萃取結構名稱
iex> %struct_name{} = %User{name: "meg"}
%User{name: "meg"}
iex> struct_name
User
二進位
二進位可能出現在使用雙小於/大於語法的模式中 (<<>>
)。模式中的二進位可以同時比對多個區段,每個區段都有不同的類型、大小和單位
iex> <<val::unit(8)-size(2)-integer>> = <<123, 56>>
"{8"
iex> val
31544
請參閱 <<>>
的文件,以取得二進位模式比對的完整定義。
最後,請記住 Elixir 中的字串是 UTF-8 編碼的二進位。這表示與字元清單類似,字串上的字首比對也可以使用二進位串接運算子 (<>
) 執行
iex> "hello " <> world = "hello world"
"hello world"
iex> world
"world"
字尾比對 (hello <> " world"
) 不是有效的模式。
防護
防護是一種使用更複雜檢查來擴充模式比對的方法。它們允許在允許模式比對的預定義建構中使用,例如函式定義、案例子句等。
並非所有表達式都允許在防護子句中,只有少數幾個允許。這是經過深思熟慮的選擇。這樣,Elixir(和 Erlang)就能確保在執行防護時不會發生任何問題,也不會在任何地方發生變異。它還允許編譯器有效率地最佳化與防護相關的程式碼。
允許的函數和運算子清單
您可以在 Kernel
模組 中找到內建的防護清單。以下是概觀
- 比較運算子 (
==
,!=
,===
,!==
,<
,<=
,>
,>=
) - 嚴格布林運算子 (
and
,or
,not
)。請注意&&
,||
和!
兄弟運算子不允許,因為它們不是嚴格布林運算子 - 這表示它們不要求參數為布林值 - 算術一元運算子 (
+
,-
) - 算術二元運算子 (
+
,-
,*
,/
) in
和not in
運算子(只要右手邊是清單或範圍)- 「類型檢查」函數 (
is_list/1
,is_number/1
等) - 在內建資料類型上運作的函數 (
abs/1
,hd/1
,map_size/1
等) map.field
語法
Bitwise
模組也包含一些 Erlang 位元運算作為防護。
由上述任何防護組合建構的巨集也是有效的防護 - 例如,Integer.is_even/1
。如需更多資訊,請參閱下方顯示的「自訂模式和防護表達式」區段。
為什麼需要防護
讓我們看一個在函數子句中使用防護的範例
def empty_map?(map) when map_size(map) == 0, do: true
def empty_map?(map) when is_map(map), do: false
防護以 when
運算子開頭,後接防護表達式。當且僅當防護表達式傳回 true
時,才會執行子句。可以使用 and
和 or
運算子來結合多個布林條件。
如果只使用模式比對來撰寫 empty_map?/1
函式是不可能的(因為對 %{}
的模式比對會比對任何一個 map,而不仅仅是空 map)。
不通過的守衛
函式子句將在且僅在它的守衛表達式評估為 true
時執行。如果傳回任何其他值,函式子句將會被略過。特別是,守衛沒有「真值」或「假值」的概念。
例如,想像一個函式檢查一個串列的開頭不是 nil
def not_nil_head?([head | _]) when head, do: true
def not_nil_head?(_), do: false
not_nil_head?(["some_value", "another_value"])
#=> false
即使串列的開頭不是 nil
,not_nil_head?/1
的第一個子句會失敗,因為表達式評估的結果不是 true
,而是 "some_value"
,因此觸發傳回 false
的第二個子句。要讓守衛正確運作,你必須確保守衛評估的結果為 true
,如下所示
def not_nil_head?([head | _]) when head != nil, do: true
def not_nil_head?(_), do: false
not_nil_head?(["some_value", "another_value"])
#=> true
守衛中的錯誤
在守衛中,當函式通常會引發例外時,它們會導致守衛失敗。
例如,tuple_size/1
函式只適用於 tuple。如果我們將它用於其他任何東西,會引發一個引數錯誤
iex> tuple_size("hello")
** (ArgumentError) argument error
然而,當用於守衛時,對應的子句將無法比對,而不是引發錯誤
iex> case "hello" do
...> something when tuple_size(something) == 2 ->
...> :worked
...> _anything_else ->
...> :failed
...> end
:failed
在許多情況下,我們可以利用這一點。在上面的程式碼中,我們使用 tuple_size/1
來檢查給定的值是否為 tuple 和 檢查它的長度(而不是使用 is_tuple(something) and tuple_size(something) == 2
)。
然而,如果你的守衛有多個條件,例如檢查 tuple 或 map,最好在 tuple_size/1
之前呼叫型別檢查函式,例如 is_tuple/1
,否則如果沒有給定 tuple,整個守衛將會失敗。或者,你的函式子句可以使用多個守衛,如下一節所示。
同一個子句中的多個守衛
還有一種方法可以簡化守衛中 or
表達式的鏈:Elixir 支援在同一個子句中撰寫「多個守衛」。以下程式碼
def is_number_or_nil(term) when is_integer(term) or is_float(term) or is_nil(term),
do: :maybe_number
def is_number_or_nil(_other),
do: :something_else
可以另寫為
def is_number_or_nil(term)
when is_integer(term)
when is_float(term)
when is_nil(term) do
:maybe_number
end
def is_number_or_nil(_other) do
:something_else
end
如果每個守衛表達式總是傳回布林值,則這兩種形式是等效的。但是,請記住,如果守衛中的任何函數呼叫引發例外,則整個守衛都會失敗。為了說明這一點,以下函數將無法偵測到空元組
defmodule Check do
# If given a tuple, map_size/1 will raise, and tuple_size/1 will not be evaluated
def empty?(val) when map_size(val) == 0 or tuple_size(val) == 0, do: true
def empty?(_val), do: false
end
Check.empty?(%{})
#=> true
Check.empty?({})
#=> false # true was expected!
這可以用來確保不會引發例外,透過類型檢查,例如 is_map(val) and map_size(val) == 0
,或使用多個守衛,這樣如果例外導致一個守衛失敗,則會評估下一個守衛。
defmodule Check do
# If given a tuple, map_size/1 will raise, and the second guard will be evaluated
def empty?(val)
when map_size(val) == 0
when tuple_size(val) == 0,
do: true
def empty?(_val), do: false
end
Check.empty?(%{})
#=> true
Check.empty?({})
#=> true
模式和守衛可用於何處
在上面的範例中,我們使用了比對運算子 (=
) 和函數子句分別展示模式和守衛。以下是 Elixir 中支援模式和守衛的內建建構清單。
match?({:ok, value} when value > 0, {:ok, 13})
函數子句
def type(term) when is_integer(term), do: :integer def type(term) when is_float(term), do: :float
case
表達式case x do 1 -> :one 2 -> :two n when is_integer(n) and n > 2 -> :larger_than_two end
匿名函數 (
fn/1
)larger_than_two? = fn n when is_integer(n) and n > 2 -> true n when is_integer(n) -> false end
for x when x >= 0 <- [1, -2, 3, -4], do: x
with
也支援else
關鍵字,它支援模式比對和守衛。try
支援catch
和else
上的模式和守衛receive
支援模式和守衛來比對接收到的訊息。自訂守衛也可以使用
defguard/1
和defguardp/1
定義。自訂守衛只能根據現有守衛定義。
請注意,比對運算子 (=
) 不 支援守衛
{:ok, binary} = File.read("some/file")
自訂模式和守衛表達式
僅此頁面中列出的建構式允許用於模式和防護中。然而,我們可以利用巨集來撰寫自訂模式防護,以簡化我們的程式或使其更具領域特定性。最後,重要的是巨集的輸出會簡化為上述建構式的組合。
例如,Elixir 中的 Record
模組提供一系列巨集,用於模式和防護中,允許在編譯期間為元組命名欄位。
為了定義您自己的防護,Elixir 甚至在 defguard
和 defguardp
中提供便利性。讓我們來看一個快速案例研究:我們想要檢查一個引數是偶數還是奇數整數。使用模式比對是不可能的,因為整數的數量是無限的,因此我們無法對每個整數進行模式比對。因此,我們必須使用防護。我們只會專注於檢查偶數,因為檢查奇數幾乎相同。
這樣的防護看起來像這樣
def my_function(number) when is_integer(number) and rem(number, 2) == 0 do
# do stuff
end
每次需要此檢查時重複撰寫會很重複。相反地,您可以使用 defguard/1
和 defguardp/1
來建立防護巨集。以下是如何執行範例
defmodule MyInteger do
defguard is_even(term) when is_integer(term) and rem(term, 2) == 0
end
然後
import MyInteger, only: [is_even: 1]
def my_function(number) when is_even(number) do
# do stuff
end
雖然可以使用巨集建立自訂防護,但建議使用 defguard/1
和 defguardp/1
來定義它們,因為它們會執行額外的編譯時間檢查。