Erlang 列表操作:[H|T] 模式
在 Erlang 中,列表是最基础、最常用的数据结构之一。而 [H|T] 是处理列表的核心模式,几乎出现在所有涉及列表的函数中。理解它,就等于掌握了 Erlang 函数式编程的钥匙。
[H|T] 并不是某种特殊语法,而是一种模式匹配(pattern matching) 的写法。它的含义是:将一个非空列表拆成两部分——H 是列表的第一个元素(Head),T 是剩下的所有元素组成的列表(Tail)。
例如,对列表 [1, 2, 3] 使用 [H|T] 模式:
H会被绑定为1T会被绑定为[2, 3]
注意:T 永远是一个列表,哪怕只剩一个元素或没有元素。
基础用法:在函数参数中直接使用 [H|T]
定义 一个函数,打印列表的第一个元素:
print_head([H|_]) ->
io:format("第一个元素是 ~p~n", [H]).
这里 _ 表示“我不关心 Tail 是什么”,这是 Erlang 中忽略无关变量的惯用法。
调用 这个函数:
print_head([a, b, c]).
% 输出:第一个元素是 a
如果传入空列表 [],这个函数会报错,因为 [] 无法匹配 [H|T] 模式。因此,处理列表时通常需要两个子句:一个处理非空列表,一个处理空列表。
完整递归模板:遍历整个列表
要安全地处理任意长度的列表(包括空列表),请按以下步骤编写函数:
- 定义 一个处理空列表的子句(递归终止条件)。
- 定义 一个处理
[H|T]的子句(递归主体)。 - 在递归主体中调用 自身处理
T。
例如,计算列表长度:
len([]) ->
0;
len([_|T]) ->
1 + len(T).
执行过程如下:
len([1,2,3])→ 匹配第二条,返回1 + len([2,3])len([2,3])→ 返回1 + len([3])len([3])→ 返回1 + len([])len([])→ 返回0- 最终结果:
1 + (1 + (1 + 0)) = 3
注意:这里我们用 _ 而不是 H,因为我们只关心元素个数,不关心具体值。
构造新列表:用 [H|T] 作为返回值
[H|T] 不仅能用于“拆解”列表,还能用于“构造”列表。这是 Erlang 中构建列表的标准方式。
例如,编写一个函数,给列表每个元素加 1:
add_one([]) ->
[];
add_one([H|T]) ->
[H + 1 | add_one(T)].
执行 add_one([1,2,3]):
- 第一层:
[1+1 | add_one([2,3])]→[2 | ...] - 第二层:
[2+1 | add_one([3])]→[3 | ...] - 第三层:
[3+1 | add_one([])]→[4 | []] - 结果:
[2,3,4]
关键点:每次递归返回一个新列表,由当前处理后的 H 和递归处理 T 的结果拼接而成。
常见错误与注意事项
-
误以为 T 是单个元素
T始终是列表。例如[x,y,z]拆解后,T是[y,z],不是y。若想取第二个元素,需再次模式匹配:[_, Second | _]。 -
忘记处理空列表
若函数只有[H|T]子句,传入[]会导致function_clause错误。务必提供空列表的处理逻辑。 -
试图修改原列表
Erlang 中所有数据都是不可变的。[H|T]不会改变原列表,而是用于创建新结构。 -
性能陷阱:在列表头部添加 vs 尾部添加
用[New | List]在头部添加元素是 O(1) 操作,高效。
但若想在尾部添加(如List ++ [New]),则是 O(n) 操作,对长列表很慢。尽量设计算法从头部构建结果。
实战:过滤偶数
编写一个函数,只保留列表中的偶数:
filter_even([]) ->
[];
filter_even([H|T]) ->
case H rem 2 of
0 -> [H | filter_even(T)]; % 是偶数,保留
_ -> filter_even(T) % 是奇数,丢弃
end.
测试:
filter_even([1,2,3,4,5,6]).
% 返回 [2,4,6]
这里展示了如何根据条件决定是否将 H 放入结果列表。
高级技巧:多层模式匹配
你可以一次性匹配多个头部元素。例如,获取前两个元素:
first_two([A, B | _]) ->
{A, B};
first_two(_) ->
error(too_short).
这比两次拆解更简洁高效。
再比如,跳过前三个元素,处理剩余部分:
skip_three([_, _, _ | Rest]) ->
process(Rest);
skip_three(_) ->
error(not_enough_elements).
性能与内存行为
Erlang 列表在内存中是以链表形式存储的,每个节点包含一个值和指向下一个节点的指针。[H|T] 模式匹配本质上是指针解引用,开销极小。
当你用 [NewH | NewT] 构造新列表时,Erlang 运行时会复用 NewT 部分的内存结构(因为数据不可变),只分配新头部节点。这种“结构共享”机制使得列表操作既安全又高效。
但要注意:如果递归过程中累积大量中间列表(如频繁使用 ++),可能导致内存膨胀。优先使用尾递归或在头部构建结果。
尾递归优化:避免栈溢出
前面的 len/1 函数不是尾递归,因为递归调用后还要执行 +1 操作。对超长列表可能耗尽调用栈。
改写为尾递归版本:
len(L) ->
len(L, 0).
len([], Acc) ->
Acc;
len([_|T], Acc) ->
len(T, Acc + 1).
这里引入累加器 Acc,每次递归更新计数,最终直接返回结果。Erlang 编译器会对这种尾递归进行优化,将其转为循环,避免栈增长。
原则:对可能处理大数据的函数,尽量写成尾递归形式。
与其他语言的对比
如果你熟悉 Python 或 JavaScript,可能会习惯用索引访问列表(如 list[0])。但在 Erlang 中,不要试图用索引。Erlang 列表没有随机访问能力,lists:nth(N, List) 是 O(n) 操作。
Erlang 的哲学是:通过模式匹配和递归,一次处理一个元素。这看似限制,实则引导你写出更清晰、更函数式的代码。
快速参考表
| 模式 | 含义 | 示例输入 | H 绑定 | T 绑定 |
|---|---|---|---|---|
[H\|T] |
非空列表拆解 | [a,b,c] |
a |
[b,c] |
[H] |
单元素列表 | [x] |
x |
[] |
[A,B\|Rest] |
取前两个 | [1,2,3,4] |
1 |
2 和 Rest = [3,4] |
[] |
空列表 | [] |
不匹配 | 不匹配 |
记住:在 Erlang 中,看到列表,就想到 [H|T]。它是你拆解、遍历、转换列表的万能工具。只要掌握模式匹配与递归这两个核心,就能优雅地处理任何列表问题。

暂无评论,快来抢沙发吧!