Erlang 模式匹配:case 与 receive
模式匹配是 Erlang 编程的基石。它贯穿于变量绑定、函数调用、流程控制等各个层面。掌握 case 和 receive 两种结构,是编写高效 Erlang 程序的关键一步。
case 表达式:流程控制的利器
case 表达式允许你根据一个值的结构进行分支处理。它的基本语法如下:
case Expression of
Pattern1 -> Result1;
Pattern2 when Guard -> Result2;
Pattern3 -> Result3
end.
核心特性:自动解构
与许多语言中的 switch 语句不同,Erlang 的 case 能够自动解构复合数据。看一个具体例子:
handle_request(Request = #{method := Method, path := Path}) ->
case Method of
GET ->
case Path of
"/home" -> {ok, "Welcome home"};
"/about" -> {ok, "About us"};
_ -> {error, "Not found"}
end;
POST ->
case maps:get(body, Request, undefined) of
undefined -> {error, "Missing body"};
Body -> {ok, Body}
end;
_ ->
{error, "Method not allowed"}
end.
注意观察:第一层 case 直接从 Request 映射中解构出 Method,第二层再解构 Path。这种嵌套写法虽然清晰,但层数多了会影响可读性。
卫语句的作用
when 关键字用于添加条件约束,这在处理数值范围时特别有用:
classify_age(Age) ->
case Age of
Age when Age < 0 -> {error, "Invalid age"};
Age when Age < 18 -> {minor, Age};
Age when Age < 65 -> {adult, Age};
Age when Age >= 65 -> {senior, Age}
end.
卫语句让代码的意图更加明确,读者一眼就能看出每个分支的适用条件。
最佳实践建议
编写 case 分支时,遵循以下原则可以大幅提升代码质量:
按优先级排列分支。将最常匹配的模式放在前面,异常情况放在后面。Erlang 会按顺序检查每个模式,合理的排序能减少不必要的检查开销。
使用下划线 _ 捕获剩余情况。每个 case 表达式都应该有一个默认分支处理所有未匹配的情况,否则当遇到意外值时会抛出异常。
保持分支简洁。如果某个分支的逻辑复杂,将其提取为单独的函数:
process_order(Order = #{items := Items}) ->
case validate_order(Order) of
{ok, Validated} ->
case calculate_total(Validated) of
{ok, Total} -> {process, Total};
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end.
validate_order(#{items := []}) -> {error, "Empty order"};
validate_order(Order) -> {ok, Order}.
receive 表达式:消息处理的核心
Erlang 的并发模型基于 Actor 模式。每个进程通过接收消息进行通信,receive 表达式正是处理这些消息的入口。
基本语法与工作原理
receive
Pattern1 -> Body1;
Pattern2 when Guard -> Body2
after
Timeout -> TimeoutBody
end.
receive 的执行过程分为三个阶段:
-
检查消息队列。进程启动时有一个空的消息队列。当其他进程向它发送消息时,这些消息会被依次放入队列。
-
尝试匹配。从队列头部开始,逐个检查消息是否匹配某个模式。如果匹配成功,该消息从队列中移除,并执行对应的代码。
-
超时处理。如果没有任何模式匹配,可以选择使用
after子句指定等待时间。after 0表示立即返回,after infinity表示无限等待。
超时机制详解
after 子句是避免进程永久阻塞的关键:
% 等待最多 5 秒,收到后返回
wait_for_response(Timeout) ->
receive
{response, Data} -> {ok, Data}
after
Timeout -> {error, timeout}
end.
% 轮询模式:检查是否有消息立即处理
poll_messages() ->
receive
{msg, M} -> {got, M}
after
0 -> empty
end.
after 0 是一个实用技巧。它会立即检查队列:如果有消息就处理,没有就继续执行后续代码。这在需要非阻塞消息检查的场景中非常有用。
选择性接收
Erlang 支持选择性接收,即只处理当前需要的消息,而将其他消息留在队列中:
handle_gossip(Pid) ->
Pid ! {self(), "Hello"},
receive
{Pid, Reply} -> {got, Reply}
after
1000 -> no_reply
end.
% 假设消息队列中有:{other, "x"}, {Pid, "Hi"}, {other, "y"}
% 上面的 receive 只会匹配第二个消息,其余两个保留在队列中
选择性接收是 Erlang 并发编程的强大特性,但它也有代价:每次接收都需要扫描队列中之前积累的消息。在高并发场景下,如果消息队列很长且频繁进行选择性接收,性能会受到影响。
消息处理的模式匹配
与 case 类似,receive 可以解构复杂的消息结构:
loop() ->
receive
{From, {add, A, B}} ->
From ! {self(), A + B},
loop();
{From, {multiply, A, B}} ->
From ! {self(), A * B},
loop();
{From, stop} ->
io:format("Stopping~n"),
ok;
{'EXIT', Pid, Reason} ->
io:format("Process ~p exited: ~p~n", [Pid, Reason]),
loop();
Message ->
io:format("Unknown message: ~p~n", [Message]),
loop()
end.
这个模式展示了几个重要技巧:
- 使用元组标记消息类型,第一元素通常是发送者 PID
- 递归调用
loop()实现持续运行 - 显式处理退出信号
'EXIT' - 使用默认模式
_捕获未知消息
case 与 receive 的组合使用
在实际应用中,receive 内部经常嵌套 case 表达式来处理复杂的业务逻辑:
-spec process_request(Request :: term()) -> {ok, Reply} | {error, Reason}.
process_request(Request) ->
receive
{From, Request} ->
Reply = case validate(Request) of
{ok, Validated} ->
case execute(Validated) of
{ok, Result} -> {ok, Result};
{error, E} -> {error, E}
end;
{error, E} -> {error, E}
end,
From ! {reply, Reply},
process_request(Request);
{From, _Other} ->
From ! {error, "Invalid request"},
process_request(Request)
after
5000 -> {error, "Timeout"}
end.
虽然这段代码功能正确,但嵌套层次过深。可以使用 case 的管道风格或提前返回来简化:
process_request(Request) ->
receive
{From, Request} ->
Reply =
try
Validated = validate(Request),
Result = execute(Validated),
{ok, Result}
catch
error:Reason -> {error, Reason}
end,
From ! {reply, Reply};
{From, _} ->
From ! {error, "Invalid request"}
after
5000 -> {error, timeout}
end.
性能与实践建议
减少消息队列长度。如果进程处理消息的速度跟不上消息到达的速度,队列会不断增长,导致选择性接收变慢。定期清理不再需要的消息,或使用背压机制让发送方放缓。
谨慎使用嵌套。三层以上的嵌套应该警惕。考虑将内层逻辑提取为独立函数,或者使用 try...catch 简化错误处理路径。
统一消息格式。定义清晰的消息协议,让所有发送方遵循相同的消息结构。这样 receive 中的模式匹配会更简洁、更安全。
利用进程字典存储状态。如果需要在多次接收之间保持状态,不要依赖消息传递来传递完整状态,而应该使用进程字典或状态记录:
loop(State = #state{counter = C}) ->
receive
increment ->
loop(State#state{counter = C + 1});
get ->
self() ! {counter, C},
loop(State);
stop -> ok
end.
总结要点
case 和 receive 是 Erlang 模式匹配在流程控制层面的两大应用。前者处理函数内部的分支逻辑,后者处理进程间的消息交互。两者都依赖模式匹配来解构数据、选择分支,这使得 Erlang 代码既简洁又表达力强。
编写时注意卫语句的使用、合理的分支顺序、以及适时的代码拆分。理解 receive 的消息队列机制和超时处理,是编写健壮并发程序的基础。

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