要点
Elixir 基础层的名词按用途分成五组:匹配与数据存取、字符串与编码、枚举与流、进程与并发、调试与工程。每个条目给签名、一句话语义、最小示例和坑,示例自包含,粘进 iex 就能跑。字典是查的不是读的——叙事式的语法讲解在《Elixir 语法要点》,本文只做条目式速查,文末速查表是全量索引。截至本文(2025 年 5 月)稳定线为 Elixir 1.18.x,版本敏感条目单独标注。
匹配与数据存取
组内共性:这组条目都围绕”形状对齐”——Elixir 里取数据的正确姿势不是防御性判空,而是声明你要的形状。叙事版见语法要点篇,这里只留查表用的签名与坑。
匹配运算符 = 与 pin ^
{:ok, value} = {:ok, 42} # 形状对齐才成功,value 绑定为 42
value = 100 # 重新绑定(rebinding),合法
^value = 101 # **MatchError:^ 断言"必须是已有值 100"**
左边是模式右边是数据;= 失败抛 MatchError,不返回 nil。
⚠️ 坑:x = x + 1 不是”自增”,是先读右边旧值再重新绑定;而 ^x = x + 1 恒等于断言 x == x + 1,永远失败。
Map.get / Map.fetch / Map.fetch!
m = %{"a" => 1}
Map.get(m, "b") # => nil(可给第三参当默认值)
Map.fetch(m, "b") # => :error(命中则 {:ok, value})
Map.fetch!(m, "b") # **KeyError**
取值三连:get 用于”缺了有默认行为”,fetch 用于”缺了要走错误分支”,fetch! 用于”缺了就是 bug,直接炸”。
⚠️ 坑:Map.get(m, "a") 返回 nil 时,分不清”键存在但值是 nil”和”键不存在”;需要区分就用 fetch。
struct 与更新语法 %{} |
defmodule User, do: defstruct([:name, :age]) # 定义具名 map
u = %User{name: "alice", age: 30}
u2 = %{u | age: 31} # 更新已有键,返回新 struct,u 不变
u2.age # => 31
更新语法 %{struct_or_map | key: new_value} 只改已存在的键。
⚠️ 坑:用它”新增”键直接抛 KeyError(BadMapError 场景之一)——新增键只能整体重建 %{u | age: 31, email: "x"} 也不行,email 不在原 struct 字段里同样报错;struct 的键集合在 defstruct 时就锁死了。
字符串与编码
组内共性:Elixir 字符串是 UTF-8 二进制(binary),”字符”是字素(grapheme)不是字节。字符列表 charlist 是另一种东西,只在和 Erlang 互操作时出现。
插值、拼接与长度
name = "elixir"
"greet: #{name}" # 插值,nil 会渲染成空串
"pre" <> "fix" # => "prefix",拼接
String.length("你好") # => 2(字素数)
byte_size("你好") # => 6(UTF-8 字节数)
插值用 #{},拼接用 <>,长度按场景二选一。
⚠️ 坑:<> 的右侧必须是字符串,"a" <> name 里 name 是 nil 或整数直接抛 ArgumentError;按字节截断中文(binary_part)会截出半个 UTF-8 序列,截字符串用 String.slice。
内置 JSON 模块(Elixir 1.18+)
JSON.encode!(%{name: "elixir", tags: ["fp", "beam"]})
1. => "{\"name\":\"elixir\",\"tags\":[\"fp\",\"beam\"]}"
JSON.decode!(~s({"a": 1})) # => %{"a" => 1}
JSON.decode(~s(not json)) # => {:error, %JSON.DecodeError{}}
1.18 起自带 JSON 编解码,底层包装 Erlang/OTP 27 的内置 JSON,基本 API 对齐 Jason 库;自定义类型的编码走 JSON.Encoder 协议,可在 defstruct 上 @derive {JSON.Encoder, only: [:id, :name]}。
⚠️ 坑:带 ! 的版本解析失败直接抛异常,适合”坏了就该炸”的场景;要拿 error tuple 用不带叹号的 JSON.decode/2。1.17 及更早没有内置 JSON,老项目仍在用第三方 Jason,两套 API 相近但模块名不同。
charlist 与 ~c
'abc' # 字符列表:整数列表 [97, 98, 99]
~c"abc" # 推荐写法,等价于上面
'abc' == "abc" # => false
charlist 是与 Erlang 老模块(如 :crypto 的部分接口)互操作时的形态,新业务代码一律用字符串。
⚠️ 坑:单引号在 Elixir 不是字符串是 charlist,复制 Erlang 例程到 Elixir 时最容易踩;1.18 的 mix format --migrate 会把旧 sigil 写法规范化成 ~c。
枚举与流
组内共性:List 是单向链表,Enum 的很多操作对大列表是 O(n);Stream 是惰性的 Enum,数据量大或要拼接多步变换时才用。
Enum 高频函数
orders = [%{id: 1, amt: 30}, %{id: 2, amt: 120}, %{id: 3, amt: 80}]
orders |> Enum.filter(&(&1.amt > 50)) |> Enum.map(& &1.id) # => [2, 3]
Enum.sum_by(orders, & &1.amt) # 1.18 新增:按函数取值求和
Enum.group_by(orders, &(&1.amt > 50)) # 按分组函数聚成 map
map/filter/reduce/sort_by/group_by 覆盖九成日常需求,全部急切求值。
⚠️ 坑:Enum.at(list, 999_999)、List.last 这类依赖位置的操作要从前走到尾,大列表是 O(n)——需要随机访问换 Map 或 :ets,别硬扛。
Stream(惰性枚举)
1..1_000_000
|> Stream.map(& &1 * 2) # 此时什么都没算
|> Stream.filter(& rem(&1, 3) == 0)
|> Enum.take(3) # 到这里才逐元素流过整条管线
多步变换 + 取前 N 个、或不想为中间结果分配内存时用 Stream;执行靠 Enum.to_list、Enum.take、Stream.run 等收尾函数触发。
⚠️ 坑:管道末尾只有 Stream 变换没有收尾函数,整条管线一行代码都不会执行——”为什么不报错也没结果”十有八九是这个。
for 推导
for n <- 1..5, rem(n, 2) == 0, into: [], do: n * 10 # => [20, 40]
1. 多生成器是笛卡尔积,不是嵌套循环语义
for x <- [1, 2], y <- [:a, :b], do: {x, y}
1. => [{1, :a}, {1, :b}, {2, :a}, {2, :b}]
带过滤条件与 into: 收集目标,适合”筛 + 映射 + 收集”一步写完的场景。
⚠️ 坑:for 是推导式不是循环,没有 break/continue;需要提前退出就说明该用 Enum.take_while 或递归了。
进程与并发
组内共性:BEAM 进程间不共享内存,消息传递是拷贝语义。开山篇用 spawn/receive 打过照面,OTP 篇讲 GenServer 封装,这里记两个最常用条目的速查。
receive 与 after
send(self(), {:ping, 1})
receive do
{:ping, n} -> n
after
1_000 -> :timeout # 1 秒没等到就走这里;没有 after 分支则永久阻塞
end
receive 从进程信箱取消息,after 给等待加超时上限(毫秒)。
⚠️ 坑:信箱里没有匹配形状的消息时,receive 会跳过不匹配的消息继续等(消息留在信箱里),漏配形状的进程看起来”卡死”其实是在等一条永远不来的消息——先 Process.info(self(), :messages) 看信箱里到底有什么。
Task.async / await
task = Task.async(fn -> heavy_work() end)
1. 中间可以做别的事
Task.await(task, 5_000) # 默认超时 5000 毫秒
一次性并发计算:起一个链接到当前进程的任务,await 收结果。
⚠️ 坑:Task.async 与调用方是链接(link)关系,任务崩了会把父进程一起带崩——不想陪葬用 Task.Supervisor.async_nolink;另外 async 了却从不 await,结果与退出消息会积压在信箱里。
调试与工程
组内共性:Elixir 的调试传统是”插到管道里看一眼”,配合交互式 shell,多数问题不需要断点调试器。
IO.inspect / dbg
" x " |> String.trim() |> IO.inspect(label: "after trim") |> String.upcase()
1. after trim: "x" ——IO.inspect 返回原值,可以插在管道任意一环
dbg(String.upcase(String.trim(" x "))) # 1.14+:打印每一步的代码与中间值
IO.inspect 是单点查看,dbg 是整条链路回放。
⚠️ 坑:IO.inspect(x) 作为一行语句调用时返回的是 :ok 不是 x——要插管道必须利用它返回原值的特性;dbg 需要 1.14+,老项目里没有。
iex 三件套与 Mix.install
# iex 里:
1. h Enum.map 查文档;i "abc" 查数据类型信息;recompile() 重编译 mix 项目
Mix.install([:jason]) # 脚本顶部声明依赖,随后直接 Jason.encode!(%{a: 1})
IO.puts("done")
h/i/recompile 覆盖查文档、查值、热重载三件高频事;Mix.install 让单文件脚本也能用 Hex 依赖。
⚠️ 坑:改了项目代码但 iex 里行为没变,是忘了 recompile();Mix.install 只属于脚本与 LiveBook 场景,正式项目依赖声明在 mix.exs,混用会得到不可复现的环境。
速查表
| 条目 | 一句话用途 | 有什么坑 |
|---|---|---|
| = 与 ^ | 形状对齐取数据 | 失败抛 MatchError;^ 是断言旧值,重绑定是另一回事 |
| Map.get / fetch / fetch! | 取值三连,按失败语义选 | get 分不清”键不存在”与”值为 nil” |
| %{s | k: v} | 更新 struct/map 已有键 | 不能新增键,struct 键集合 defstruct 时锁死 |
| 插值与 | 字符串拼装 | 右侧必须是字符串;字节级截断会切碎 UTF-8 |
| String.length vs byte_size | 字素数与字节数 | 中文按字节截断是半个字符 |
| JSON(1.18+) | 内置编解码,包装 OTP 27 JSON | decode! 抛异常;1.17 及以前要用 Jason 库 |
| ~c”abc” | charlist,Erlang 互操作 | ‘abc’ != “abc”,单引号不是字符串 |
| Enum | 急切枚举全家桶 | 链表按位置访问是 O(n) |
| Stream | 惰性管线省中间分配 | 没有收尾函数整条管线不执行 |
| for | 筛 + 映射 + 收集一步写完 | 是推导式不是循环,没有 break |
| receive / after | 收消息 + 超时上限 | 不匹配的消息留在信箱,等不到时先查信箱 |
| Task.async / await | 一次性并发任务 | 默认 5 秒超时;崩了连带父进程(要隔离用 async_nolink) |
| IO.inspect / dbg | 管道插桩与链路回放 | 单独调用返回 :ok;dbg 是 1.14+ |
| iex h/i/recompile | 查文档/查值/热重载 | 忘 recompile 以为代码没生效 |
| Mix.install | 单文件脚本用 Hex 依赖 | 只属于脚本场景,项目依赖在 mix.exs |
下一步
基础层查完,往上两层:并发与容错的条目(GenServer 回调、Supervisor 策略、Registry)在《OTP 与进程》里按同一套三段式展开;Web 层的条目(LiveView 的 assign/stream、路由)结合实战看《用 Phoenix LiveView 做一个实时看板》。开山篇(Elixir 与 Phoenix 入门)仍是整个系列的定位入口。
版本核实(以官方文档为准):稳定线为 Elixir 1.18.x(1.18.0 发布于 2024-12-19,内置 JSON 模块与 JSON.Encoder 协议、Enum.sum_by、mix format –migrate 为 1.18 特性;1.18.3 发布于 2025-03-06),运行时对应 Erlang/OTP 27 线;dbg 自 1.14、Mix.install 自 1.12、Task 与 receive/after 为早期即有的稳定 API;Task.await 默认超时 5000 毫秒、Supervisor 默认重启强度以官方文档为准。本文未引用后续尚未发布的版本特性。