查看源代码 模式和守卫

Elixir 提供模式匹配,允许我们断言数据结构的形状或从中提取值。模式通常用守卫增强,使开发人员能够执行更复杂的检查,尽管有限制。

本文档提供了模式和守卫的完整参考,包括它们的语义、允许使用的位置以及如何扩展它们。

模式

Elixir 中的模式由变量、字面量和特定于数据结构的语法组成。最常用的执行模式匹配的结构之一是匹配运算符 (=)

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 支持重新绑定。如果你不希望变量的值改变,可以使用固定运算符 (^)

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] 符号匹配非空列表,该符号匹配列表的 headtail

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"}

最后,请注意模式中的映射键必须始终是字面量或使用固定运算符匹配的先前绑定变量。

结构体

结构体可以使用百分号、结构体模块名称或变量后跟花括号语法 (%{}) 出现在模式中。

给定以下结构体

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") 不是有效的模式。

守卫

守卫是一种用更复杂的检查来增强模式匹配的方法。它们允许出现在一组预定义的结构中,这些结构允许模式匹配,例如函数定义、case 子句等。

并非所有表达式都允许在守卫子句中使用,而只是一小部分。这是一个刻意为之的选择。这样,Elixir(和 Erlang)可以确保在执行守卫时不会发生任何不良事件,并且不会在任何地方发生变异。它还允许编译器有效地优化与守卫相关的代码。

允许的函数和运算符列表

你可以在 Kernel 模块 中找到内置的守卫列表。以下是概述

  • 比较运算符 (==, !=, ===, !==, <, <=, >, >=)
  • 严格布尔运算符 (and, or, not)。注意 &&, ||! 运算符**不允许**,因为它们不是严格布尔型 - 意味着它们不需要参数是布尔型
  • 算术一元运算符 (+, -)
  • 算术二元运算符 (+, -, *, /)
  • innot 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 时,才会执行该子句。可以使用 andor 运算符组合多个布尔条件。

仅使用模式匹配来编写 empty_map?/1 函数是不可能的(因为 %{} 的模式匹配将匹配任何映射,而不仅仅是空映射)。

不通过的守卫

当且仅当函数子句的守卫表达式评估为 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

即使列表的头部不是 nilnot_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 函数只适用于元组。如果我们对其他任何内容使用它,将引发参数错误

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 同时检查给定值是否是元组以及检查其大小(而不是使用 is_tuple(something) and tuple_size(something) == 2)。

但是,如果你的守卫有多个条件,例如检查元组或映射,最好在 tuple_size/1 之前调用类型检查函数,例如 is_tuple/1,否则如果未提供元组,整个守卫将失败。或者,你的函数子句可以使用多个守卫,如下一节所示。

同一子句中的多个守卫

在守卫中简化 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?/2:

    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
  • forwith<- 左侧支持模式和守卫

    for x when x >= 0 <- [1, -2, 3, -4], do: x

    with 还支持 else 关键字,它支持模式匹配和守卫。

  • trycatchelse 上支持模式和守卫

  • receive 支持模式和守卫来匹配接收到的消息。

  • 自定义守卫也可以使用 defguard/1defguardp/1 定义。自定义守卫只能基于现有的守卫来定义。

请注意,匹配运算符 (=) *不支持* 守卫

{:ok, binary} = File.read("some/file")

自定义模式和守卫表达式

只有此页面中列出的构造允许在模式和守卫中使用。但是,我们可以利用宏来编写自定义模式守卫,这些守卫可以简化我们的程序或使它们更特定于域。归根结底,重要的是宏的*输出*归结为上面构造的组合。

例如,Elixir 中的 Record 模块提供了一系列宏,这些宏可在模式和守卫中使用,这些宏允许元组在编译期间具有命名字段。

为了定义您自己的守卫,Elixir 甚至在 defguarddefguardp 中提供了便利。让我们看一个快速案例研究:我们要检查参数是否为偶数或奇数整数。使用模式匹配这是不可能的,因为有无穷多个整数,因此我们无法匹配每个整数。因此,我们必须使用守卫。我们将只关注检查偶数,因为检查奇数几乎相同。

这样的守卫看起来像这样

def my_function(number) when is_integer(number) and rem(number, 2) == 0 do
  # do stuff
end

每次我们需要进行此检查时编写它会很重复。相反,您可以使用 defguard/1defguardp/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/1defguardp/1 来定义它们,因为它们会执行额外的编译时检查。