Elixir 语法要点:模式匹配是一等公民

要点

前置阅读是开山篇:BEAM 进程模型与 Phoenix 跑通流程已讲过,本篇只讲语言核心。Elixir 的语法坎不在”看不懂”,而在”看着像、其实不是”——等号不是赋值、变量不可变却可以重新绑定、管道不只是语法糖。按”模式匹配 → 不可变 → 管道 → 数据结构 → 控制流与函数”的顺序过掉高频误解,末尾交代 1.18 类型检查现状。截至本文(2025 年 5 月)稳定线为 Elixir 1.18.x,示例均可在 iex 直接运行。

模式匹配:等号是断言不是赋值

= 在 Elixir 里叫匹配运算符(match operator):左边是模式,右边是数据,两边”形状”对得上才成功,同时把变量绑定到对应位置。看一组对照:

{:ok, value} = {:ok, 42}      # 匹配成功,value 被绑定为 42
{:ok, value} = {:error, :x}   # 匹配失败,抛 MatchError

[user, %{age: age}] = ["alice", %{age: 30}]   # 嵌套结构逐层对齐
age                                           # => 30

x = 1          # 第一次:把 1 绑给 x
x = x + 1      # 合法!重新绑定(rebinding),不是"修改"x 指向的数据

两个容易漏的点。第一,单独一个变量能匹配任何东西:x = {:ok, 1} 永远成功,x 绑的是整个元组;想让 x 断言”必须是已有值”而不是重新绑定,用 pin 运算符 ^(如 {:ok, ^expected} = ...)。第二,case 从上往下逐个试子句,最后一个子句往往是兜底。反模式点名:随手加裸变量子句把意外形状吞掉——_ -> nil 这类兜底会把”数据结构变了”这种本该炸出来的 bug 变成安静的 nil。兜底子句要写明预期的错误形状,真意外就让它崩,OTP 篇讲监督树时会再呼应。

不可变数据与重新绑定

Elixir 的数据结构不可变(immutable):任何”修改”操作都返回新值,原值原封不动。字符串替换是最直白的例子:

s = "hello"
String.replace(s, "l", "L")   # => "heLLo",s 仍是 "hello",原值不变

初学者常问”每次都拷贝,性能不就完了”。BEAM 底层有结构共享(structure sharing):大部分”新值”与旧值共享未变的部分,是设计内成本而非全量复制。要适应的是写法:没有循环内改累加变量的习惯,取而代之是递归携带新状态或交给 Enum.reduce。

对照站内写过的 PHP 常驻内存路线(Workerman 常驻内存编程):常驻 PHP 里全局数组、静态属性被多个请求路径改来改去,是脏状态与内存泄漏的头号来源,只能靠纪律规避;Elixir 把这条路焊死——数据传进函数就改不了,要变化就返回新值。代价是多一层”返回什么”的表达,收益是没有函数能在你看不见的地方改掉你的数据。

管道运算符:把上一步结果插到下一步第一个参数

|> 的语义一句话:把左侧表达式的值,作为右侧函数调用的第一个参数。它解决嵌套调用的可读性:

# 嵌套写法,从内往外读:
String.upcase(String.trim("  elixir  "))

1. 管道写法,从上往下读:
"  elixir  "
|> String.trim()
|> String.upcase()        # => "ELIXIR"

反模式点名:对”第一个参数不是主数据”的函数用管道。Map.merge(map_a, map_b) 键冲突时右侧覆盖左侧,x |> Map.merge(other) 读不出到底谁覆盖谁;比较运算更典型,x |> Kernel.(>)(5) 不如直接写 x > 5。管道是给”数据流过一串变换”的场景用的,参数语义模糊就退回普通调用。

常用数据结构速查

Elixir 没有用户自定义的”数组与哈希”二分,高频类型是下表六个,先认清字面量再谈用法:

类型 字面量示例 典型用途 高频坑
原子 atom :ok, :error 状态标签、模块名、消息标签 不被 GC 回收,动态 String.to_existing_atom 而非 String.to_atom,外部输入转 atom 有内存风险
字符串 string “hello” 文本,UTF-8 二进制 与字符列表 charlist 完全是两种东西,见下
字符列表 charlist ‘hello’(单引号) 与 Erlang 老代码互操作 'abc' == "abc" 为 false;新代码写 charlist 用 ~c"abc",1.18 的 mix format --migrate 会自动改写
元组 tuple {:ok, 42} 固定结构返回值、模式匹配载体 按整块存取,改中间元素是 O(n) 拷贝,别当数组用
列表 list [1, 2, 3] 遍历、枚举场景 单向链表:头部插入 O(1),取”最后一个”是 O(n);没有随机访问
映射 map %{“a” => 1},%{a: 1} 键值数据、配置、结构化传参 键无序;原子键的 %{a: 1} 是 %{:a => 1} 的语法糖,两种键混用时易晕

两个补充。keyword list([timeout: 5_000, retries: 2])本质是”原子键的列表”,有序、允许重复键,函数调用的选项参数用它。struct(defstruct 定义的具名 map)用点号 user.name 访问,键不存在直接抛错;对 struct 用方括号反而因未实现 Access 协议报错——点号与方括号在 map(缺键返回 nil)和 struct(点号抛错、方括号不可用)上的差异是真实的坑。

控制流:没有 for 和 while

Elixir 没有 for/while 循环语句,重复靠递归与 Enum,分支靠四个工具,分工各有主场:

构造 主场 一句话边界
case 按”数据的形状”分支 首选;子句按顺序匹配,匹配不到抛 CaseClauseError
cond 按”布尔条件”分支 相当于 if/else if 链,最后一支 true -> 当 else 用
if / unless 单条件二分支 条件必须是布尔值,if x 传非布尔会直接抛错(与多数语言不同)
with 串一串”可能失败的步骤” 左边 <- 逐个匹配,任一步不中模式就整体走 else;省掉层层嵌套 case

with 值得单独看一眼,它把”成功路径拉直”,任一步失配就整体走 else:

with {:ok, user} <- fetch_user(id),
     {:ok, order} <- fetch_order(user),
     {:ok, _} <- charge(order) do
  {:ok, "charged"}
else
  {:error, reason} -> {:error, reason}   # 各步失败的形状在这里分流
end

反模式点名:把 with 当 case 用——只串一步,或 else 里塞满与 <- 左侧毫无对应的分支。它的价值在”三步以上、每步返回 {:ok, _} / {:error, _} 形状”的串行流程,两步以内用 case 更直白。

函数定义:子句、默认参数与守卫

Elixir 的函数是”一组子句”:同名函数按模式匹配分发到不同实现,模式匹配的第二个主场。

defmodule Greeter do
  1. 同名多子句:按参数形状分发,比函数体内 case 更常用
  def greet(%{name: name}), do: "Hi, #{name}!"
  def greet(nil), do: "Hi, stranger!"

  1. 守卫(guard)给子句加类型级条件;自定义守卫用 defguard
  defguard is_adult(age) when is_integer(age) and age >= 18
  def vote(age) when is_adult(age), do: :allowed
  def vote(_age), do: :denied
end

三处注意:默认参数写双反斜杠 \\(如 def join(list, sep \\ ", "));守卫里只能用白名单内的表达式,不能调任意函数;模块是命名空间不是类,defstruct 给的是数据结构而非”实例方法”,行为挂函数、数据挂 struct,不绑死。

1.18 类型检查现状:帮手,不是保险箱

1.18(2024 年 12 月发布)的函数调用类型检查是这两年最值得知道的语言层变化:编译期能查出”给函数传了不可能的参数类型””case 里有永远匹配不上的模式””元组按越界索引取值”等问题。但定位要说准:这是渐进式类型系统(gradual set-theoretic types)的第一步,for 推导式、with、闭包暂不在检查范围内。它对既有代码零破坏(只加警告不报错),把”1.18 起类型安全”理解成”编译器开始帮你盯调用约定”更符合现实,别拿它当 Haskell 的编译期保证用。

下一步

语言核心过完,两种读法:要一份能查的索引,读《Elixir 基础知识参考字典》;要进入并发与容错主线(GenServer、Supervisor、let it crash),读《OTP 与进程》——那是 Elixir 区别于其他语言的核心价值。

版本核实(以官方文档为准):Elixir 稳定线为 1.18.x(1.18.0 发布于 2024-12-19,函数调用类型检查与 mix format --migrate 为本篇涉及的 1.18 特性),运行时对应 Erlang/OTP 27 线;with 的 else 子句自 1.3 起可用,非 1.18 新增;结构共享的实际开销本站未实测,以官方口径为准。本文未引用后续尚未发布的版本特性。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇