强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Erlang/OTP 完全指南 / 08 - 元组与 Map

第 08 章:元组与 Map — 记录与数据结构

本章深入元组(Tuple)、记录(Record)和 Map 的使用,它们是 Erlang 中表示结构化数据的核心工具。


8.1 元组(Tuple)

8.1.1 基本操作

%% 创建元组
Point = {10, 20}.
Person = {person, "Alice", 25, "Beijing"}.

%% 元素访问(1-indexed)
element(1, Point).         %% 10
element(2, Point).         %% 20
element(3, Person).        %% 25

%% 替换元素(返回新元组)
NewPoint = setelement(1, Point, 100).  %% {100, 20}
Point.  %% 仍然是 {10, 20}

%% 元组大小
tuple_size(Point).         %% 2
tuple_size(Person).        %% 4

%% 转换为列表
tuple_to_list(Person).     %% [person, "Alice", 25, "Beijing"]

%% 从列表创建
list_to_tuple([1, 2, 3]).  %% {1, 2, 3}

%% 追加元素(O(n))
erlang:append_element(Point, 30).  %% {10, 20, 30}

8.1.2 元组的模式匹配

%% 基本解构
{X, Y} = {10, 20}.  %% X=10, Y=20

%% 标签元组模式(Erlang 传统风格)
{person, Name, Age, City} = Person.
%% Name="Alice", Age=25, City="Beijing"

%% 嵌套解构
{point, {X, Y}, {W, H}} = {point, {10, 20}, {100, 200}}.
%% X=10, Y=20, W=100, H=200

%% 通配符
{person, Name, _, _} = Person.
%% Name="Alice"

8.1.3 元组作为"记录"

在 Erlang 传统中,用标签元组表示结构化数据:

%% 用元组表示用户
User = {user, "Alice", 25, "alice@example.com"}.

%% 提取字段
{user, Name, Age, Email} = User.

%% 用元组表示结果
ok = {ok, Value}.           %% 成功结果
error = {error, Reason}.    %% 错误结果

%% 常见模式
case file:read_file("data.txt") of
    {ok, Data} -> process(Data);
    {error, Reason} -> io:format("Error: ~p~n", [Reason])
end.

8.1.4 元组的性能特征

操作时间复杂度说明
element(N, Tuple)O(1)常数时间访问
setelement(N, Tuple, Val)O(n)需要复制整个元组
tuple_size(Tuple)O(1)常数时间
erlang:append_element/2O(n)需要复制整个元组

💡 元组适合小而固定大小的数据。大而频繁修改的数据用 Map。


8.2 记录(Record)

8.2.1 定义记录

记录是元组的语法糖,编译时转换为元组:

%% 在头文件中定义 record
%% include/user.hrl
-record(user, {
    name = "",           %% 默认值
    age = 0,
    email = "",
    active = true
}).

%% 或在模块中定义
-module(my_module).
-record(person, {
    name,
    age,
    city = "unknown"
}).

8.2.2 创建记录

%% 需要先包含头文件或在模块中定义
-include("user.hrl").

%% 使用默认值创建
U1 = #user{}.
%% {user, "", 0, "", true}

%% 指定字段创建
U2 = #user{name = "Alice", age = 25, email = "alice@example.com"}.
%% {user, "Alice", 25, "alice@example.com", true}

%% 指定部分字段(其他用默认值)
U3 = #user{name = "Bob"}.
%% {user, "Bob", 0, "", true}

8.2.3 访问记录字段

%% 使用 #record.field 语法
Name = U2#user.name.       %% "Alice"
Age = U2#user.age.         %% 25

%% 模式匹配
#user{name = Name, age = Age} = U2.
%% Name="Alice", Age=25

%% 部分匹配
#user{name = Name} = U2.
%% Name="Alice"

8.2.4 更新记录

%% 更新字段(创建新记录,原记录不变)
U4 = U2#user{age = 26, city = "Beijing"}.
%% {user, "Alice", 26, "alice@example.com", true}

%% U2 不变
U2#user.age.  %% 25

8.2.5 记录的局限性

局限说明
需要编译时定义不能动态添加字段
字段顺序固定编译时确定
不支持模式匹配更新不能直接 X#user{name = get_name()}
必须导入/包含定义跨模块需要 include hrl 文件
动态字段访问困难需要 element(N, Record)

💡 对于需要动态字段的场景,使用 Map。

8.2.6 记录转换为 Map 和反向

%% Record -> Map
record_to_map(#user{} = U) ->
    #{
        name => U#user.name,
        age => U#user.age,
        email => U#user.email,
        active => U#user.active
    }.

%% Map -> Record
map_to_record(#{name := N, age := A, email := E, active := Act}) ->
    #user{name = N, age = A, email = E, active = Act}.

8.3 Map

8.3.1 创建 Map

%% Map 字面量(使用 => 操作符)
M1 = #{name => "Alice", age => 25}.
M2 = #{1 => "one", 2 => "two", 3 => "three"}.

%% 混合键类型
M3 = #{atom_key => 1, "string_key" => 2, 42 => 3}.

%% 空 Map
Empty = #{}.
map_size(Empty).  %% 0

%% 使用 maps 模块
M4 = maps:from_list([{name, "Alice"}, {age, 25}]).
%% #{name => "Alice", age => 25}

8.3.2 访问 Map 值

M = #{name => "Alice", age => 25, city => "Beijing"}.

%% 语法一:#{} 操作符(不存在会崩溃)
Val = M#{name}.     %% "Alice"
M#{height}.         %% 错误!key not found

%% 语法二:maps:get/2(不存在会崩溃)
maps:get(name, M).          %% "Alice"

%% 语法三:maps:get/3(带默认值)
maps:get(height, M, 0).     %% 0(不存在时返回默认值)

%% 语法四:maps:find/2(返回 {ok, Val} 或 error)
maps:find(name, M).         %% {ok, "Alice"}
maps:find(height, M).       %% error

%% 语法五:maps:is_key/2(检查是否存在)
maps:is_key(name, M).       %% true
maps:is_key(height, M).     %% false

8.3.3 更新 Map

M = #{name => "Alice", age => 25}.

%% 添加/更新字段(=> 用于 put 操作)
M2 = M#{city => "Beijing"}.
%% #{name => "Alice", age => 25, city => "Beijing"}

%% 更新已存在的字段(:= 用于 update 操作)
M3 = M#{age := 26}.
%% #{name => "Alice", age => 26}

%% ❌ 错误:用 := 更新不存在的字段
M#{height := 170}.  %% 错误!key not found

%% ✅ 正确:用 => 添加新字段
M#{height => 170}.

%% maps:put/3
M4 = maps:put(age, 26, M).

%% maps:remove/2
M5 = maps:remove(age, M).
%% #{name => "Alice"}

%% maps:merge/2
M6 = maps:merge(#{a => 1}, #{b => 2, a => 3}).
%% #{a => 3, b => 2}(第二个 map 优先)

8.3.4 Map 模式匹配

M = #{name => "Alice", age => 25, city => "Beijing"}.

%% 部分匹配(只需要匹配你关心的 key)
#{name := Name} = M.                    %% Name = "Alice"
#{name := Name, age := Age} = M.        %% Name="Alice", Age=25

%% 函数参数中的 Map 匹配
greet(#{name := Name}) ->
    io:format("Hello, ~s!~n", [Name]).

handle_request(#{method := get, path := "/users"}) ->
    list_users();
handle_request(#{method := post, path := "/users", body := Body}) ->
    create_user(Body);
handle_request(#{method := _, path := Path}) ->
    {error, {not_found, Path}}.

%% 注意:=> 用于创建/put,:= 用于匹配/update
%% #{key => value}     创建 map 或添加字段
%% #{key := Value}     模式匹配或更新已有字段

8.3.5 Map 操作速查

函数作用示例
maps:new()空 Map#{}
maps:put(K, V, M)添加/更新M#{K => V}
maps:get(K, M)获取值(不存在崩溃)M#{K}
maps:get(K, M, Default)获取值(带默认)maps:get(K, M, 0)
maps:find(K, M)查找{ok, V} | error
maps:is_key(K, M)是否存在true | false
maps:remove(K, M)删除M#{K := undefined}
maps:merge(M1, M2)合并M2 优先
maps:keys(M)所有键[K1, K2, ...]
maps:values(M)所有值[V1, V2, ...]
maps:to_list(M)转列表[{K,V}, ...]
maps:from_list(L)从列表创建#{K=>V, ...}
maps:map(F, M)映射值#{K => F(K,V)}
maps:filter(F, M)过滤#{K => V}
maps:fold(F, Acc, M)折叠累积
maps:size(M)大小integer()
maps:iterator(M)迭代器用于遍历
maps:next(Iter)迭代下一步与 iterator 配合

8.3.6 Map 遍历

M = #{a => 1, b => 2, c => 3}.

%% 方式一:maps:foreach/2(遍历,无返回值)
maps:foreach(fun(K, V) ->
    io:format("~p => ~p~n", [K, V])
end, M).

%% 方式二:maps:fold/3(折叠,有返回值)
Sum = maps:fold(fun(_K, V, Acc) -> V + Acc end, 0, M).
%% 6

%% 方式三:转列表后操作
[{K, V} || {K, V} := M].
%% [{a,1},{b,2},{c,3}]

%% 方式四:使用 maps:iterator(高效遍历大 Map)
Iter = maps:iterator(M),
遍历(Iter) ->
    case maps:next(Iter) of
        {K, V, NextIter} ->
            io:format("~p => ~p~n", [K, V]),
            遍历(NextIter);
        none ->
            done
    end.

8.3.7 Map vs 元组列表 vs 记录

特性Map元组列表记录
键类型任意固定位置编译时字段名
动态字段
访问速度O(log n)O(n)O(1)
模式匹配✅ 部分匹配
内存占用较高较低最低
序列化方便方便不方便
推荐场景通用数据结构简单 KV固定结构

8.4 实战:用户管理系统

%% user_manager.erl
-module(user_manager).
-export([new/0, add_user/4, get_user/2, update_user/3,
         remove_user/2, list_active/1, find_by_city/2]).

-type user() :: #{
    id := integer(),
    name := string(),
    age := integer(),
    city := string(),
    active := boolean()
}.
-type user_db() :: #{integer() => user()}.

-spec new() -> user_db().
new() -> #{}.

-spec add_user(integer(), string(), integer(), string()) -> fun((user_db()) -> user_db()).
add_user(Id, Name, Age, City) ->
    fun(Db) ->
        User = #{id => Id, name => Name, age => Age, city => City, active => true},
        Db#{Id => User}
    end.

-spec get_user(integer(), user_db()) -> {ok, user()} | {error, not_found}.
get_user(Id, Db) ->
    case maps:find(Id, Db) of
        {ok, User} -> {ok, User};
        error -> {error, not_found}
    end.

-spec update_user(integer(), map(), user_db()) -> user_db().
update_user(Id, Updates, Db) ->
    case maps:find(Id, Db) of
        {ok, User} ->
            UpdatedUser = maps:merge(User, Updates),
            Db#{Id => UpdatedUser};
        error ->
            Db
    end.

-spec remove_user(integer(), user_db()) -> user_db().
remove_user(Id, Db) ->
    maps:remove(Id, Db).

-spec list_active(user_db()) -> [user()].
list_active(Db) ->
    maps:fold(fun(_Id, #{active := true} = User, Acc) -> [User | Acc];
                 (_Id, _, Acc) -> Acc
              end, [], Db).

-spec find_by_city(string(), user_db()) -> [user()].
find_by_city(City, Db) ->
    [User || {_Id, #{city := C} = User} := Db, C =:= City].
$ erl
1> c(user_manager).
{ok, user_manager}
2> Db0 = user_manager:new().
#{}
3> Db1 = (user_manager:add_user(1, "Alice", 25, "Beijing"))(Db0).
4> Db2 = (user_manager:add_user(2, "Bob", 30, "Shanghai"))(Db1).
5> {ok, User} = user_manager:get_user(1, Db2).
{ok,#{active => true,age => 25,city => "Beijing",id => 1,name => "Alice"}}
6> user_manager:find_by_city("Beijing", Db2).
[#{active => true,age => 25,city => "Beijing",id => 1,name => "Alice"}]

8.5 实战:配置管理器

%% config.erl
-module(config).
-export([load/1, get/2, get/3, set/3, to_list/1]).

-type config() :: #{atom() => term()}.

-spec load([{atom(), term()}]) -> config().
load(Defaults) ->
    maps:from_list(Defaults).

-spec get(atom(), config()) -> term().
get(Key, Config) ->
    maps:get(Key, Config).

-spec get(atom(), config(), term()) -> term().
get(Key, Config, Default) ->
    maps:get(Key, Config, Default).

-spec set(atom(), term(), config()) -> config().
set(Key, Value, Config) ->
    Config#{Key => Value}.

-spec to_list(config()) -> [{atom(), term()}].
to_list(Config) ->
    maps:to_list(Config).

8.6 二进制中的元组模式

%% 二进制模式匹配与元组结合
<<A:16/little, B:16/big>> = <<1, 0, 0, 2>>.
%% A = 1(小端序), B = 2(大端序)

%% 解析结构化二进制数据
parse_record(<<Type:8, Length:16, Data:Length/binary, _CRC:32>>) ->
    #{type => Type, data => Data}.

8.7 注意事项

⚠️ 常见陷阱

陷阱说明
=> vs :=创建用 =>,更新/匹配用 :=
Record 不是 MapRecord 编译时展开为元组,无法动态扩展
元组更新是 O(n)大元组频繁更新应该换用 Map
Map 键顺序Map 按键排序遍历(小 Map 保持插入序)
记录作用域Record 定义需要在使用前导入(include)

💡 最佳实践

  1. 固定结构数据 → Record(性能最好)
  2. 动态 key-value → Map(最灵活)
  3. 简单 KV 列表 → Proplist [{K,V}](最轻量)
  4. 模式匹配 → Map 比 Record 更方便
  5. 序列化/JSON → Map 最方便

8.8 扩展阅读


上一章:07 - 列表深入 下一章:09 - 并发编程