文章目录

Erlang 列表操作:[H|T] 模式

发布于 2026-04-04 01:27:38 · 浏览 2 次 · 评论 0 条

Erlang 列表操作:[H|T] 模式

在 Erlang 中,列表是最基础、最常用的数据结构之一。而 [H|T] 是处理列表的核心模式,几乎出现在所有涉及列表的函数中。理解它,就等于掌握了 Erlang 函数式编程的钥匙。

[H|T] 并不是某种特殊语法,而是一种模式匹配(pattern matching) 的写法。它的含义是:将一个非空列表拆成两部分——H 是列表的第一个元素(Head),T 是剩下的所有元素组成的列表(Tail)。

例如,对列表 [1, 2, 3] 使用 [H|T] 模式:

  • H 会被绑定为 1
  • T 会被绑定为 [2, 3]

注意:T 永远是一个列表,哪怕只剩一个元素或没有元素。


基础用法:在函数参数中直接使用 [H|T]

定义 一个函数,打印列表的第一个元素:

print_head([H|_]) ->
    io:format("第一个元素是 ~p~n", [H]).

这里 _ 表示“我不关心 Tail 是什么”,这是 Erlang 中忽略无关变量的惯用法。

调用 这个函数:

print_head([a, b, c]).
% 输出:第一个元素是 a

如果传入空列表 [],这个函数会报错,因为 [] 无法匹配 [H|T] 模式。因此,处理列表时通常需要两个子句:一个处理非空列表,一个处理空列表。


完整递归模板:遍历整个列表

要安全地处理任意长度的列表(包括空列表),请按以下步骤编写函数:

  1. 定义 一个处理空列表的子句(递归终止条件)。
  2. 定义 一个处理 [H|T] 的子句(递归主体)。
  3. 在递归主体中调用 自身处理 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 的结果拼接而成


常见错误与注意事项

  1. 误以为 T 是单个元素
    T 始终是列表。例如 [x,y,z] 拆解后,T[y,z],不是 y。若想取第二个元素,需再次模式匹配:[_, Second | _]

  2. 忘记处理空列表
    若函数只有 [H|T] 子句,传入 [] 会导致 function_clause 错误。务必提供空列表的处理逻辑。

  3. 试图修改原列表
    Erlang 中所有数据都是不可变的。[H|T] 不会改变原列表,而是用于创建新结构。

  4. 性能陷阱:在列表头部添加 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 2Rest = [3,4]
[] 空列表 [] 不匹配 不匹配

记住:在 Erlang 中,看到列表,就想到 [H|T]。它是你拆解、遍历、转换列表的万能工具。只要掌握模式匹配与递归这两个核心,就能优雅地处理任何列表问题。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文