文章目录

Erlang 模式匹配:case 与 receive

发布于 2026-04-04 11:35:51 · 浏览 17 次 · 评论 0 条

Erlang 模式匹配:case 与 receive

模式匹配是 Erlang 编程的基石。它贯穿于变量绑定、函数调用、流程控制等各个层面。掌握 casereceive 两种结构,是编写高效 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 的执行过程分为三个阶段:

  1. 检查消息队列。进程启动时有一个空的消息队列。当其他进程向它发送消息时,这些消息会被依次放入队列。

  2. 尝试匹配。从队列头部开始,逐个检查消息是否匹配某个模式。如果匹配成功,该消息从队列中移除,并执行对应的代码。

  3. 超时处理。如果没有任何模式匹配,可以选择使用 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.

总结要点

casereceive 是 Erlang 模式匹配在流程控制层面的两大应用。前者处理函数内部的分支逻辑,后者处理进程间的消息交互。两者都依赖模式匹配来解构数据、选择分支,这使得 Erlang 代码既简洁又表达力强。

编写时注意卫语句的使用、合理的分支顺序、以及适时的代码拆分。理解 receive 的消息队列机制和超时处理,是编写健壮并发程序的基础。

评论 (0)

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

扫一扫,手机查看

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