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, "[email protected]"}.
%% 提取字段
{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/2 | O(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 = "[email protected]"}.
%% {user, "Alice", 25, "[email protected]", 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, "[email protected]", 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 不是 Map | Record 编译时展开为元组,无法动态扩展 |
| 元组更新是 O(n) | 大元组频繁更新应该换用 Map |
| Map 键顺序 | Map 按键排序遍历(小 Map 保持插入序) |
| 记录作用域 | Record 定义需要在使用前导入(include) |
💡 最佳实践
- 固定结构数据 → Record(性能最好)
- 动态 key-value → Map(最灵活)
- 简单 KV 列表 → Proplist
[{K,V}](最轻量) - 模式匹配 → Map 比 Record 更方便
- 序列化/JSON → Map 最方便
8.8 扩展阅读
- 📖 maps module
- 📖 Erlang Reference Manual - Records
- 📖 Erlang Reference Manual - Maps
- 📖 Learn You Some Erlang - Maps