05 - 模式匹配
第 05 章:模式匹配
模式匹配(Pattern Matching)是 Erlang 编程的灵魂。本章深入讲解函数子句、case 表达式、守卫(Guard)和递归中的模式匹配。
5.1 模式匹配基础
5.1.1 = 操作符
在 Erlang 中,= 不是赋值,而是模式匹配(Pattern Match):
%% 匹配成功:左侧模式匹配右侧值
X = 42. %% X 绑定为 42
{A, B} = {1, 2}. %% A=1, B=2
[H|T] = [1,2,3]. %% H=1, T=[2,3]
%% 匹配失败:抛出 badmatch 异常
X = 43.
%% ** exception error: no match of right hand side value 43
%% 两边都已绑定时,检查是否相等
X = 42. %% ok(X 已经是 42)
%% 模式匹配可以解构任意复杂的数据
#{name := Name, age := Age} = #{name => "Alice", age => 25}.
%% Name="Alice", Age=25
5.1.2 匹配的规则
| 规则 | 示例 |
|---|---|
| 字面量必须完全相等 | 42 = 42 ✅, 42 = 43 ❌ |
| 未绑定变量匹配任意值 | X = 42 → X=42 |
| 已绑定变量检查相等 | X = X ✅(如果 X 已绑定) |
_ 匹配任意值(不绑定) | {_, Y} = {1, 2} |
| 结构必须匹配 | {A, B} = {1, 2, 3} ❌ |
列表用 | 分割头部和尾部 | `[H |
5.2 函数子句
5.2.1 多子句函数
%% area/1 的多个子句
area({circle, R}) ->
math:pi() * R * R;
area({rect, W, H}) ->
W * H;
area({triangle, B, H}) ->
B * H / 2.
1> area({circle, 5}).
78.53981633974483
2> area({rect, 4, 6}).
24
3> area({triangle, 3, 8}).
12.0
4> area({square, 4}).
** exception error: no function clause matching area({square,4})
5.2.2 函数子句匹配顺序
函数子句从上到下依次匹配,第一个匹配成功的子句被执行:
%% 子句顺序很重要!
classify(X) when is_number(X), X > 0 -> positive;
classify(X) when is_number(X), X < 0 -> negative;
classify(0) -> zero;
classify(_) -> not_a_number.
5.2.3 参数解构
%% 解构元组参数
handle({connect, Host, Port}) ->
io:format("Connecting to ~s:~p~n", [Host, Port]);
handle({disconnect, Reason}) ->
io:format("Disconnected: ~p~n", [Reason]);
handle({data, Data}) ->
io:format("Received: ~p~n", [Data]).
%% 解构列表参数
process([]) ->
empty;
process([Single]) ->
{one_element, Single};
process([First, Second]) ->
{two_elements, First, Second};
process([H|T]) ->
{head, H, tail_length, length(T)}.
%% 解构嵌套结构
process_message({chat, #{from := From, to := To, body := Body}}) ->
io:format("~s -> ~s: ~s~n", [From, To, Body]);
process_message({system, Level, Msg}) ->
io:format("[~p] ~s~n", [Level, Msg]).
5.3 守卫(Guard)
5.3.1 基本守卫
%% when 关键字引入守卫
is_positive(X) when X > 0 -> true;
is_positive(_) -> false.
%% 多条件守卫(逗号 = 且)
in_range(X, Min, Max) when is_number(X), X >= Min, X =< Max ->
true;
in_range(_, _, _) ->
false.
%% 分号 = 或
is_adult({person, _, Age}) when Age >= 18; Age < 0 ->
true;
is_adult(_) ->
false.
5.3.2 守卫表达式
| 守卫表达式 | 说明 |
|---|---|
X > Y | 大于 |
X < Y | 小于 |
X >= Y | 大于等于 |
X =< Y | 小于等于 |
X =:= Y | 等于(严格) |
X =/= Y | 不等于(严格) |
X == Y | 等于(自动转换类型) |
X /= Y | 不等于(自动转换类型) |
is_atom(X) | 类型检查 |
is_integer(X) | 类型检查 |
is_list(X) | 类型检查 |
is_map(X) | 类型检查 |
is_pid(X) | 类型检查 |
is_function(X, N) | 检查是否是 N 元函数 |
is_record(X, Tag, Size) | 检查 record |
element(N, Tuple) | 取元组第 N 个元素 |
hd(List) | 列表头 |
tl(List) | 列表尾 |
length(List) | 列表长度 |
abs(X) | 绝对值 |
round(X) | 四舍五入 |
trunc(X) | 截断 |
size(X) | 元组或二进制大小 |
bit_size(X) | 位串大小 |
byte_size(X) | 二进制字节数 |
5.3.3 守卫限制
⚠️ 注意:守卫中不能调用自定义函数,只能使用 BIF 和比较操作
%% ❌ 错误:守卫中不能调用自定义函数
is_valid(X) when my_module:check(X) -> true.
%% ✅ 正确:在函数体内调用
handle(X) ->
case my_module:check(X) of
true -> do_something(X);
false -> error
end.
%% 守卫中不能使用 and/or,使用逗号和分号代替
%% ❌ when X > 0 and X < 10
%% ✅ when X > 0, X < 10
5.3.4 =:= 与 == 的区别
%% =:= 严格相等(推荐)
1 =:= 1.0. %% false
1 == 1.0. %% true(类型转换)
%% 最佳实践:始终使用 =:= 和 =/=
%% 避免类型转换带来的隐式行为
5.4 case 表达式
5.4.1 基本用法
%% case 语法
describe(Number) ->
case Number of
0 -> "zero";
1 -> "one";
2 -> "two";
_ -> "many"
end.
%% case 可以匹配任意模式
process(Result) ->
case Result of
{ok, Data} ->
io:format("Success: ~p~n", [Data]);
{error, Reason} ->
io:format("Error: ~p~n", [Reason]);
timeout ->
io:format("Operation timed out~n")
end.
5.4.2 case 中的守卫
classify_temperature(Temp) ->
case Temp of
T when T < 0 -> freezing;
T when T < 15 -> cold;
T when T < 25 -> comfortable;
T when T < 35 -> warm;
_ -> hot
end.
5.4.3 变量作用域规则
⚠️ 关键规则:case 的每个分支必须绑定相同的变量集合
%% ❌ 错误:不同分支绑定不同变量
case X of
{a, Y} -> Y; %% 绑定 Y
{b, Z} -> Z %% 绑定 Z(Y 未绑定)
end.
%% 编译错误:unsafe variable 'Y'
%% ✅ 正确:在 case 前预绑定
Y = undefined,
Z = undefined,
case X of
{a, Y} -> Y;
{b, Z} -> Z
end.
%% ✅ 更好的方式:使用嵌套函数或明确赋值
handle(X) ->
Result = case X of
{a, Val} -> Val;
{b, Val} -> Val
end,
Result.
5.5 if 表达式
5.5.1 基本用法
%% if 表达式(类似其他语言的 if-else)
abs_value(X) ->
if
X >= 0 -> X;
true -> -X %% true 是"默认分支"(相当于 else)
end.
%% 多条件
grade(Score) ->
if
Score >= 90 -> 'A';
Score >= 80 -> 'B';
Score >= 70 -> 'C';
Score >= 60 -> 'D';
true -> 'F'
end.
5.5.2 if 的守卫条件
%% if 的每个分支都是守卫条件
check(X) ->
if
is_atom(X) -> "atom";
is_integer(X) -> "integer";
is_list(X) -> "list";
true -> "unknown"
end.
💡 提示:if 表达式不常用,大多数情况下 case + 模式匹配更清晰。
5.6 递归中的模式匹配
5.6.1 列表处理
%% 求列表长度(递归)
my_length([]) -> 0;
my_length([_H|T]) -> 1 + my_length(T).
%% 列表求和
sum([]) -> 0;
sum([H|T]) -> H + sum(T).
%% 列表反转
reverse([]) -> [];
reverse([H|T]) -> reverse(T) ++ [H].
%% 注意:这种写法效率低(O(n²)),实际使用 lists:reverse/1
5.6.2 列表过滤
%% 过滤偶数
filter_even([]) -> [];
filter_even([H|T]) when H rem 2 =:= 0 ->
[H | filter_even(T)];
filter_even([_H|T]) ->
filter_even(T).
%% 通用过滤器
filter(_Pred, []) -> [];
filter(Pred, [H|T]) ->
case Pred(H) of
true -> [H | filter(Pred, T)];
false -> filter(Pred, T)
end.
%% 使用
1> filter(fun(X) -> X > 3 end, [1,2,3,4,5]).
[4,5]
5.6.3 嵌套结构匹配
%% 递归处理树结构
-type tree() :: {node, tree(), tree()} | {leaf, term()}.
sum_tree({leaf, Value}) ->
Value;
sum_tree({node, Left, Right}) ->
sum_tree(Left) + sum_tree(Right).
%% 使用
1> Tree = {node, {leaf, 1}, {node, {leaf, 2}, {leaf, 3}}}.
2> sum_tree(Tree).
6
5.7 高级模式匹配技巧
5.7.1 嵌套模式匹配
%% 同时匹配多层结构
handle_event({click, {button, Id}, #{x := X, y := Y}}) ->
io:format("Button ~p clicked at (~p, ~p)~n", [Id, X, Y]).
%% 匹配带默认值的 map
process(#{name := Name, age := Age, role := Role}) ->
io:format("~s (~p): ~s~n", [Name, Age, Role]);
process(#{name := Name, age := Age}) ->
process(#{name => Name, age => Age, role => "user"}).
5.7.2 = 操作符在模式中的使用
%% 在模式中使用 = 绑定整个子结构
handle({person, _} = Person) ->
%% Person 绑定了整个 {person, _} 元组
process_person(Person).
%% 更复杂的例子
analyze([{tag, Tag} = Entry | Rest]) ->
%% Entry 绑定整个 {tag, Tag} 元组
io:format("Tag: ~p, Full entry: ~p~n", [Tag, Entry]),
analyze(Rest);
analyze([]) ->
ok.
5.7.3 二进制模式匹配
%% 解析二进制协议
%% 格式: [版本:8][类型:8][长度:16][数据:binary]
parse_packet(<<Version:8, Type:8, Length:16, Data:Length/binary>>) ->
{ok, #{version => Version, type => Type, data => Data}};
parse_packet(_) ->
{error, invalid_packet}.
%% 解析 IPv4 地址
parse_ipv4(<<A:8, B:8, C:8, D:8>>) ->
io_lib:format("~p.~p.~p.~p", [A, B, C, D]).
%% HTTP 状态行解析
parse_status_line(<<"HTTP/", Version:5/binary, " ", Code:3/binary, " ", Reason/binary>>) ->
{Version, binary_to_integer(Code), binary_to_list(Reason)}.
5.7.4 Map 模式匹配
%% Map 部分匹配(只匹配需要的 key)
greet(#{name := Name}) ->
io:format("Hello, ~s!~n", [Name]).
%% 带守卫的 Map 匹配
check_access(#{role := admin}) ->
full_access;
check_access(#{role := user, level := Level}) when Level >= 5 ->
limited_access;
check_access(#{role := user}) ->
read_only;
check_access(_) ->
no_access.
%% 使用
1> greet(#{name => "Alice", age => 25}).
Hello, Alice!
ok
2> check_access(#{role => admin}).
full_access
5.8 实战:命令解析器
%% command_parser.erl
-module(command_parser).
-export([parse/1, execute/1]).
-type command() ::
{say, string()} |
{move, atom()} |
{attack, string()} |
{look, string() | all} |
quit.
-spec parse(string()) -> {ok, command()} | {error, string()}.
parse(Input) ->
case string:lexemes(string:trim(Input), " ") of
["say" | Words] ->
{ok, {say, string:join(Words, " ")}};
["move", Direction] ->
case parse_direction(Direction) of
{ok, Dir} -> {ok, {move, Dir}};
error -> {error, "Invalid direction: " ++ Direction}
end;
["attack", Target] ->
{ok, {attack, Target}};
["look"] ->
{ok, {look, all}};
["look", Target] ->
{ok, {look, Target}};
["quit"] ->
{ok, quit};
[] ->
{error, "Empty command"};
_ ->
{error, "Unknown command"}
end.
-spec execute(command()) -> string().
execute({say, Message}) ->
"You say: " ++ Message;
execute({move, north}) -> "You move north. A dark corridor.";
execute({move, south}) -> "You move south. An open field.";
execute({move, east}) -> "You move east. A river blocks your path.";
execute({move, west}) -> "You move west. A mountain rises ahead.";
execute({attack, Target}) ->
"You attack " ++ Target ++ "!";
execute({look, all}) ->
"You see: a sword, a shield, and a potion.";
execute({look, Target}) ->
"You examine: " ++ Target;
execute(quit) ->
"Goodbye!".
-spec parse_direction(string()) -> {ok, atom()} | error.
parse_direction("north") -> {ok, north};
parse_direction("south") -> {ok, south};
parse_direction("east") -> {ok, east};
parse_direction("west") -> {ok, west};
parse_direction(_) -> error.
$ erl
1> c(command_parser).
{ok, command_parser}
2> {ok, Cmd} = command_parser:parse("say hello world").
3> command_parser:execute(Cmd).
"You say: hello world"
4> {ok, Cmd2} = command_parser:parse("move north").
5> command_parser:execute(Cmd2).
"You move north. A dark corridor."
5.9 实战:JSON 解析器(简化版)
%% mini_json.erl
-module(mini_json).
-export([parse/1]).
%% 简化的 JSON 解析器(演示模式匹配)
parse(<<${, Rest/binary>>) ->
parse_object(Rest, #{});
parse(<<$[, Rest/binary>>) ->
parse_array(Rest, []);
parse(<<$", Rest/binary>>) ->
parse_string(Rest, []).
parse_object(<<$}, Rest/binary>>, Acc) ->
{Acc, Rest};
parse_object(<<$\s, Rest/binary>>, Acc) ->
parse_object(Rest, Acc);
parse_object(<<$,, Rest/binary>>, Acc) ->
parse_object(Rest, Acc);
parse_object(<<$", Rest/binary>>, Acc) ->
{Key, Rest2} = parse_string(Rest, []),
{Value, Rest3} = parse_value(skip_whitespace(Rest2)),
parse_object(Rest3, Acc#{Key => Value}).
parse_array(<<$], Rest/binary>>, Acc) ->
{lists:reverse(Acc), Rest};
parse_array(<<$\s, Rest/binary>>, Acc) ->
parse_array(Rest, Acc);
parse_array(<<$, , Rest/binary>>, Acc) ->
parse_array(Rest, Acc);
parse_array(Data, Acc) ->
{Value, Rest} = parse_value(skip_whitespace(Data)),
parse_array(Rest, [Value | Acc]).
parse_string(<<$", Rest/binary>>, Acc) ->
{lists:reverse(Acc), Rest};
parse_string(<<C, Rest/binary>>, Acc) ->
parse_string(Rest, [C | Acc]).
parse_value(<<$", Rest/binary>>) -> parse_string(Rest, []);
parse_value(<<${, Rest/binary>>) -> parse_object(Rest, #{});
parse_value(<<$[, Rest/binary>>) -> parse_array(Rest, []).
skip_whitespace(<<$\s, Rest/binary>>) -> skip_whitespace(Rest);
skip_whitespace(<<$\n, Rest/binary>>) -> skip_whitespace(Rest);
skip_whitespace(<<$\t, Rest/binary>>) -> skip_whitespace(Rest);
skip_whitespace(Data) -> Data.
5.10 注意事项
⚠️ 常见陷阱
| 陷阱 | 说明 |
|---|---|
| 变量作用域 | case 分支必须绑定相同的变量 |
| =:= vs == | 始终使用 =:= 避免隐式类型转换 |
| 子句顺序 | 函数子句从上到下匹配,顺序很重要 |
| 守卫副作用 | 守卫不能有副作用(不能调用自定义函数) |
| 未处理的模式 | 确保所有可能的情况都被覆盖 |
💡 最佳实践
- 优先使用模式匹配 + 函数子句代替 case
- 使用守卫的逗号
,(且)和分号;(或) - 始终包含通配符
_分支处理意外情况 - 二进制模式匹配是处理网络协议的利器
- 利用
=在模式中绑定子结构
5.11 扩展阅读
- 📖 Erlang Reference Manual - Pattern Matching
- 📖 Erlang Reference Manual - Guards
- 📖 Learn You Some Erlang - Pattern Matching
上一章:04 - 变量与类型 下一章:06 - 函数进阶