news 2026/10/1 10:08:17

Elixir 模式匹配与守卫完整参考:语义、适用位置与自定义扩展

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Elixir 模式匹配与守卫完整参考:语义、适用位置与自定义扩展
  • 编程语言
  • 编译器
  • 标准库
  • 语言运行时
  • 并发编程

【免费下载链接】elixir

Simple from zero to scale

项目地址:https://gitcode.com/GitHub_Trending/el/elixir
点击查看免费下载

本文是 Elixir 官方参考文档《Patterns and guards》的深度技术指南,完整覆盖模式匹配(pattern matching)与守卫(guards)的语义规则、允许使用的构造、适用位置以及基于defguard/1的自定义扩展方法。读者阅读后将能准确掌握=匹配运算符、pin 运算符、各数据结构的匹配规则,理解守卫与普通表达式的本质区别(无副作用、可优化、异常即失败),并在实际项目中熟练运用match?/2、函数子句、case、with与自定义守卫。

Elixir 通过模式匹配让我们可以断言数据的形状或从中提取值,而守卫则进一步提供了更复杂(但有严格限制)的检查能力。本文全部内容以仓库中的官方参考文档 patterns-and-guards.md 为主体,并结合 Elixir 源码与测试用例进行印证。

模式匹配(Patterns)

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 支持重新绑定(rebinding)。如果不想让变量的值改变,可以使用 pin 运算符(^):

iex> x = 1 1 iex> ^x = 2 ** (MatchError) no match of right hand side value: 2

这里^x表示匹配x的当前值(即1),而不是重新绑定,因此与2匹配失败。从源码来看,pin 运算符匹配的是值而非模式,其相等性判断基于===/2严格比较,相关语义在 kernel.ex 的 pin 运算符文档(约第 3576 行、3604 行)中有明确说明。

如果同一个变量在同一个模式中出现多次,那么所有出现处都必须绑定到相同的值:

iex> {x, x} = {1, 1} {1, 1} iex> {x, x} = {1, 2} ** (MatchError) no match of right hand side value: {1, 2}

然而,一个变量不能在同一模式中通过自身来定义。这包括变量同时出现在嵌套匹配运算符两侧的情况:

{:ok, x} = {x, :ok} %{a: value} = %{b: value}

这类模式会被拒绝为"循环变量定义"(cyclic variable definitions)。正确的改写方式是:先匹配一次,再使用守卫或单独的==比较来施加额外约束。

下划线变量(_)具有特殊含义:它永远不会被绑定到任何值。当你不在意模式中的某个值时它特别有用:

iex> {_, integer} = {:not_important, 1} {:not_important, 1} iex> integer 1 iex> _ ** (CompileError) iex:3: invalid use of _

被 pin 的值代表该值本身,而不是一个(即使语法上相同的)模式。右侧的值需要与 pin 的值相等:

iex> x = %{} %{} iex> {:ok, %{}} = {:ok, %{a: 13}} {:ok, %{a: 13}} iex> {:ok, ^x} = {:ok, %{a: 13}} ** (MatchError) no match of right hand side value: {:ok, %{a: 13}}

注意:%{}模式可以匹配任何映射(包括含a: 13的映射),但被 pin 的^x只匹配与x的值(空映射%{})严格相等的值。

字面量(数字与原子)

原子和数字(整数与浮点数)可以出现在模式中,并且总是按自身表示。例如,原子只有当与它自身相同才会匹配:

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} # 因大小不同而不匹配 iex> {:ok, integer} = {:ok, 11, 13} ** (MatchError) no match of right hand side value: {:ok, 11, 13} # 因第一个元素不匹配 iex> {:ok, binary} = {:error, :enoent} ** (MatchError) no match of right hand side value: {:error, :enoent}

{:ok, result}这种"标签 + 值"的匹配模式是 Elixir 代码中最常见的惯用法之一,广泛用于函数返回值(如File.read/1)与with表达式的解构。

列表

列表可以使用方括号语法([])出现在模式中。模式中的列表只匹配相同大小的列表,且每个元素也必须各自匹配:

iex> [:ok, integer] = [:ok, 13] [:ok, 13] # 因大小不同而不匹配 iex> [:ok, integer] = [:ok, 11, 13] ** (MatchError) no match of right hand side value: [:ok, 11, 13] # 因第一个元素不匹配 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: []

由于字符列表(charlist)表示为整数列表,因此也可以使用列表拼接运算符++对字符列表执行前缀匹配:

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

二进制

二进制可以使用双尖括号语法(<<>>)出现在模式中。模式中的二进制可以同时匹配多个段(segment),每个段可以有不同的类型、大小和单位:

iex> <<val::unit(8)-size(2)-integer>> = <<123, 56>> "{8" iex> val 31544

其中unit(8)指定每个单元为 8 位,size(2)指定共 2 个单元,integer指定该段为整数类型,因此val被解析为123 * 256 + 56 = 31544。二进制的完整模式匹配定义可查阅<<>>/1运算符的文档。

另外请记住:Elixir 中的字符串是 UTF-8 编码的二进制。因此与字符列表类似,字符串也可以用二进制拼接运算符<>进行前缀匹配:

iex> "hello " <> world = "hello world" "hello world" iex> world "world"

后缀匹配(如hello <> " world")不是合法的模式。

守卫(Guards)

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

并非所有表达式都能出现在守卫子句中,只有少数几种允许。这是有意为之的设计:通过这种方式,Elixir(经由 Erlang)确保所有守卫都是可预测的(没有突变或其他副作用),并且可以被优化、高效执行。

允许的函数与操作符清单

内置守卫的完整清单定义在Kernel模块中(见 kernel.ex 的## Guards文档章节,约第 148 行起)。概览如下:

  • 比较运算符(==、!=、===、!==、<、<=、>、>=),以及max/2、min/2
  • 严格布尔运算符and/2、or/2、not/1。注意&&、||和!这组同族运算符不允许出现在守卫中,因为它们不是严格布尔运算——它们不要求参数必须是布尔值
  • 一元算术运算符+/1、-/1
  • 二元算术运算符+/2、-/2、*/2、//2
  • in/2与not in/2运算符(前提是右侧为列表或范围)
  • "类型检查"函数(is_list/1、is_number/1等)
  • 作用于内置数据类型上的函数(abs/1、hd/1、map_size/1等)
  • map.field语法

此外,Bitwise模块(见 bitwise.ex)也包含一批可作守卫使用的 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/2与or/2组合。

仅靠模式匹配是无法写出这个empty_map?/1函数的——因为在%{}上做模式匹配会匹配任何映射,而不只是空映射(正如上文映射一节所述,空映射模式匹配所有映射)。

非布尔结果的守卫(Non-passing guards)

函数子句当且仅当其守卫表达式求值为true时才会执行。如果返回任何其他值,该函数子句都会被跳过。特别地,守卫没有"truthy"(真值)或"falsy"(假值)的概念。

例如,想象一个检查列表头部是否不是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只对元组有效。如果对其它类型使用,会抛出参数错误:

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)。

不过,如果守卫有多个条件(例如同时检查元组或映射),最好先调用is_tuple/1这类类型检查函数,再调用tuple_size/1,否则一旦传入的不是元组,整个守卫都会失败。另一种方案是使用下文介绍的"同一子句中的多重守卫"。

同一子句中的多重守卫

Elixir 提供了一种简化守卫中or表达式链的额外方式:在同一个子句中编写"多重守卫"。以下代码:

def categorize_number(term) when is_integer(term) or is_float(term) or is_nil(term), do: :maybe_number def categorize_number(_other), do: :something_else

可以改写为:

def categorize_number(term) when is_integer(term) when is_float(term) when is_nil(term) do :maybe_number end def categorize_number(_other) do :something_else end

如果每个守卫表达式始终返回布尔值,这两种形式是等价的。但请回顾:如果守卫中的任何函数调用抛出了异常,整个守卫都会失败。为了说明这一点,下面的函数将无法检测空元组:

defmodule Check do # 如果传入元组,map_size/1 会抛异常,tuple_size/1 将不会被求值 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!

这可以通过确保不抛异常来修正,要么使用类型检查(如is_map(val) and map_size(val) == 0),要么使用多重守卫——这样即使某个守卫因异常失败,下一个守卫仍会被求值:

defmodule Check do # 如果传入元组,map_size/1 会抛异常,第二个守卫仍会被求值 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

核心区别:or链中一旦某个守卫抛异常,整个守卫短路失败;而多重守卫(when依次排列)中每个守卫独立求值,前一个失败会继续尝试下一个。这是两种写法在语义上最重要的差异。

模式与守卫可以在哪里使用

在之前的例子中,我们分别用匹配运算符=和函数子句展示了模式与守卫。以下是 Elixir 内置的、支持模式与守卫的全部构造:

  • match?/2:

    match?({:ok, value} when value > 0, {:ok, 13})

    match?/2是Kernel中的宏(见 kernel.ex 第 3614 行)。从源码可以看出,它本质上把模式编译成一个case表达式:模式匹配成功返回true,否则返回false,不会抛出MatchError,非常适合用在断言与过滤场景。

  • 函数子句:

    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):

    larger_than_two? = fn n when is_integer(n) and n > 2 -> true n when is_integer(n) -> false end
  • for与with支持在<-左侧使用模式与守卫:

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

但你不能在=左侧写when守卫。

自定义模式与守卫表达式

只有本页列出的构造才允许出现在模式与守卫中。然而,我们可以利用宏来编写自定义的模式/守卫表达式,以简化程序或使其更具领域特定性。归根结底,重要的是宏的输出必须归结为上述构造的组合。

例如,Elixir 的Record模块(见 record.ex)提供了一系列可在模式与守卫中使用的宏,允许元组在编译期拥有命名字段。

用defguard/1与defguardp/1定义自定义守卫

Elixir 为定义自己的守卫提供了defguard/1与defguardp/1便利设施。来看一个快速案例研究:我们希望检查一个参数是否为偶数或奇数整数。用模式匹配这是不可能的——因为整数有无限多个,我们不可能对每一个都写一个模式。因此必须使用守卫。这里只关注偶数检查,奇数几乎完全一样。

这样的守卫看起来像:

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会在编译期对守卫表达式进行校验,若使用了守卫子句不允许的表达式,将在编译期直接抛出异常(guard_test.exs中对此有专门的测试用例,见 guard_test.exs)。defguardp/1定义的是模块私有的守卫宏,规则类似defmacrop/2:必须在当前模块中使用之前定义。

标准库中的defguard实践

Elixir 标准库自身就大量使用defguard。例如Integer.is_odd/1与Integer.is_even/1在 integer.ex 中的实现为:

defguard is_odd(integer) when is_integer(integer) and (integer &&& 1) == 1 defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0

注意这里用位运算&&&(来自Bitwise模块)实现奇偶判断,而本文案例中用rem/2取余实现——两种方式都是合法的守卫表达式。这印证了defguard的确可以组合任意"已允许的守卫构造"。

defguard的底层实现位于 kernel.ex 第 5922 行起:defguard生成一个宏,该宏在守卫内外都能使用,并保持各自语义——在守卫内部调用时,相当于把when右侧的表达式注入守卫;在守卫外部调用时,保持普通函数调用语义(所有参数在调用前求值,未使用的参数则不求值)。Kernel模块文档还建议,定义自定义守卫时参考 naming-conventions.md 中关于布尔守卫命名(is_前缀)的约定。

虽然用宏也能创建自定义守卫,但推荐使用defguard/1与defguardp/1,因为它们会执行额外的编译期检查,确保自定义守卫合法且可被编译器优化。

小结

  • 模式匹配是解构数据的基础工具:变量总是可重新绑定,^pin 运算符固定已有值,_永不绑定;元组与列表要求大小完全一致,映射与结构体执行子集匹配(结构体还要求键必须合法),二进制与字符串支持多段匹配与<>前缀匹配。
  • 守卫是对模式匹配的受控增强:只允许无副作用的纯函数集合(比较、严格布尔、算术、类型检查、in、map.field等),守卫中的异常导致子句失败而非抛出,且不存在 truthy/falsy 语义——只有true才会放行。
  • 适用位置覆盖match?/2、函数子句、case、fn、for/with、try、receive,但=运算符不支持守卫。
  • 扩展之道是defguard/1与defguardp/1:它们把合法守卫表达式封装为宏,兼具编译期校验与守卫内外的双重可用性,标准库的Integer.is_even/1就是最佳范例。

如需进一步学习,可继续阅读仓库中的相关文档:operators.md(运算符优先级)、naming-conventions.md(命名约定)、typespecs.md(类型规格),以及 kernel.ex 中## Guards章节列出的全部内置守卫清单。

  • 编程语言
  • 编译器
  • 标准库
  • 语言运行时
  • 并发编程

【免费下载链接】elixir

Simple from zero to scale

项目地址:https://gitcode.com/GitHub_Trending/el/elixir
点击查看免费下载

相关推荐

上一篇:基于 TileLang 的 HISA 分层稀疏注意力 Prefill 索引器实现剖析
下一篇:使用 TileLang 在 AMD MI300X 上实现高性能 FlashMLA:从 Hopper 到 CDNA3 的架构适配与优化实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 10:08:15

MCP协议重构Excel工作流:AI智能代理与pandas协同实战

1. 项目概述&#xff1a;这不是写个脚本&#xff0c;而是给Excel装上AI神经中枢 “开发自己的第一个MCP——用 AI 智能重构 Excel 处理工作流”&#xff0c;这标题里藏着三个被多数人忽略的硬核事实&#xff1a;第一&#xff0c;“MCP”不是某个新出的Python库&#xff0c;而是…

作者头像 李华
网站建设 2026/10/1 10:04:50

服务运营|MS‘26:强化学习+OR优化外卖平台的展示排序和路径规划

编者按&#xff1a; 你打开外卖App&#xff0c;看到一排餐厅。 你可能以为&#xff0c;这个顺序主要由广告、销量、评分、距离&#xff0c;或者平台想推哪家店决定。但从运营管理的角度看&#xff0c;排序还有另一层含义&#xff1a;它会改变订单未来要走多远、能不能顺路合单、…

作者头像 李华
网站建设 2026/10/1 10:04:14

GPT-6 Astra真正吓人的,不是99.9%:AI开始自己把活干完了

2026 年 9 月 3 日凌晨&#xff0c;OpenAI 扔下 GPT-6 Astra&#xff0c;整个科技圈连夜无眠。刷屏最快的是两张图&#xff1a;一张是 ARC-AGI-3 测试上刺眼的 99.9%&#xff0c;另一张是总裁 Greg Brockman 那句掷地有声的 “Welcome to the AGI era.”&#xff08;欢迎来到 A…

作者头像 李华
网站建设 2026/10/1 10:04:00

基于Springboot的火车售票系统设计与实现(源码+文档+部署讲解等)

联系博主 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 …

作者头像 李华