导读
到目前为止,我们已经讨论了两种数据容器,分别是元组和列表。元组用于保存固定数量的 元素,而列表用于保存可变数量的元素。
本章将介绍记录(record)和映射组(map)。记录其实就是元组的另一种形式。通过使用记 录,可以给元组里的各个元素关联一个名称。
映射组是键-值对的关联性集合。键可以是任意的Erlang数据类型。它们在Perl和Ruby里被称 为散列(hash),在C++和Java里被称为映射(map),在Lua里被称为表(table),在Python里则被 称为字典(dictionary)。
使用记录和映射组能让编程更容易。与其记住某个数据项在复杂数据结构里的存放位置,不 如使用该项的名称,让系统找到数据存放的位置。记录使用一组固定且预定义的名称,而映射组 可以动态添加新的名称。
一. 何时使用映射组或记录
记录其实就是元组的另一种形式,因此它们的存储与性能特性和元组一样。映射组比元组占 用更多的存储空间,查找起来也更慢。而另一方面,映射组比元组要灵活得多。
应该在下列情形里使用记录:
-
当你可以用一些预先确定且数量固定的原子来表示数据时;
-
当记录里的元素数量和元素名称不会随时间而改变时;
-
当存储空间是个问题时,典型的案例是你有一大堆元组,并且每个元组都有相同的结构。 映射组适合以下的情形:
-
当键不能预先知道时用来表示键-值数据结构;
-
当方便使用很重要而效率无关紧要时作为万能的数据结构使用;
-
用作“自解释型”的数据结构,也就是说,用户容易从键名猜出值的含义;
-
用来表示键-值解析树,例如XML或配置文件;
-
用JSON来和其他编程语言通信。
二. 通过记录命名元组里的项
对于小型元组而言,记住各个元素代表什么几乎不成问题,但当元组包含大量元素时,给各 个元素命名就更方便了。一旦命名了这些元素,就可以通过名称来指向它们,而不必记住它们在 元组里的具体位置。
用记录声明来命名元组里的元素,它的语法如下:
-record(Name, { %% 以下两个键带有默认值 key1 = Default1, key2 = Default2, ... %% 下一行就相当于Key 3 = undefined key3, ... }). %%% 警告:record不是一个shell命令(在shell里要用rr,详见本节后面的描述)。记录声明只能在 Erlang源代码模块里使用,不能用于shell
在之前的例子里,Name是记录名。key1、key2这些是记录所含各个字段的名称,它们必须 是原子。记录里的每个字段都可以带一个默认值,如果创建记录时没有指定某个字段的值,就会 使用默认值。
举个例子,假设想要操作一个待办事项列表。我们会首先定义一个todo记录,然后将它保 存在一个文件里(记录的定义既可以保存在Erlang源代码文件里,也可以由扩展名为.hrl的文件 保存,然后包含在Erlang源代码文件里)。
请注意,文件包含是唯一能确保多个Erlang模块共享相同记录定义的方式。它类似于C语言 用.h文件保存公共定义,然后包含在源代码文件里。
% records.hrl -record(todo, {status=reminder,who=joe,text}). % 记录一旦被定义,就可以创建该记录的实例了。要在shell里这么做,必须先把记录的定义读入shell,然后才能创建记录。我们将用shell函数 rr(read records的缩写,即读取记录)来实现。 rr("records.hrl"). % [todo]
-
创建和更新记录
> rr("records.hrl") > #todo{}. % #todo{status = reminder,who = joe,text = undefined} > X2 = #todo{status=done}. % #todo{status = done,who = joe,text = undefined} > X1 = #todo{status=urgent, text="Fix errata in book"}. %%% 我们在第2行和第3行创建了新的记录。语法#todo{key1=Val1, ..., keyN=ValN}用于创 建一个类型为todo的新纪录。所有的键都是原子,而且必须与记录定义里所用的一致。如果省略 了一个键,系统就会用记录定义里的值作为该键的默认值。在第4行复制了一个现有的记录。语法X1#todo{status=done}的意思是创建一个X1的副本 (类型必须是todo),并修改字段status的值为done。请记住,这么做生成的是原始记录的一个副本,原始记录没有变化。
-
提取记录字段
% 要在一次操作中提取记录的多个字段,可以使用模式匹配 > #todo{who=W, text=Txt} = X1. % #todo{status = aa,who = joe,text = undefined} > X1#todo.text. % undefined
-
在函数里模式匹配记录
我们可以编写模式匹配记录字段或者创建新记录的函数,代码如下所示
clear_status(#todo{status=S, who=W} = R) -> %% 在此函数内部,S和W绑定了记录里的字段值 %% values in the record %% %% R是*整个*记录 R#todo{status=finished} %% ... % 要匹配某个类型的记录,可以这样编写函数定义 do_something(X) when is_record(X, todo) -> %% ... 这个子句会在X是todo类型的记录时匹配成功。
-
记录是元组的另一种形式
> X1. % #todo{status = aa,who = joe,text = undefined} % 现在我们要让shell忘掉todo的定义 10> rf(todo). > X1. % {todo,aa,joe,undefined} %%% 在第10行里,rf(todo)命令使shell忘了todo记录的定义。因此,现在打印X2时,shell将X2 显示成一个元组。其实它们在系统内部都是元组,但记录提供了方便的语法,让你可以用名称而 非位置来指明不同的元素。
三. 映射组:关联式键-值存储
映射组从Erlang的R17版开始可供使用。 映射组具有下列属性。
-
映射组的语法与记录相似,不同之处是省略了记录名,并且键值分隔符是=>或:=。
-
映射组是键-值对的关联性集合。
-
映射组里的键可以是任何全绑定的Erlang数据类型(即数据结构里没有任何未绑定变量)。
-
映射组里的各个元素根据键进行排序。
-
在不改变键的情况下更新映射组是一种节省空间的操作。
-
查询映射组里某个键的值是一种高效的操作。
-
映射组有着明确的顺序。 我们将在下面几节里更详细地介绍映射组。
-
映射组语法
% 映射组的写法依照以下语法: #{Key1 Op Val1, Key2 Op Val2, ..., KeyN Op ValN}. % 它的语法与记录相似,但是散列符号(即#)之后没有记录名,而Op是=>或者:=这两个符号的其中一个。 % 键和值可以是任何有效的Erlang数据类型。 % 举个例子,假设要创建一个包含a、b两个键的映射组。 > F1 = #{a => 1, b=> 2}. % 创建一个带有非原子键的映射组 Facts = #{{wife,fred} => "Sue", {age,fred} => 45, {daughter,fred} => "Mary", {likes, jim} => [...]}. % 映射组在系统内部是作为有序集合存储的,打印时总是使用 各键 排序后的顺序,与映射组的 创建方式无关。这里有一个例子: F2 = #{ b => 2, a=>1 }. % #{a => 1,b => 2} F1 = F2. % #{a => 1,b => 2} %要基于现有的映射组更新一个映射组,我们会使用如下语法,其中的Op(更新操作符)是 =>或:= NewMap = OldMap # { K1 Op V1,...,Kn Op Vn } % 表达式K => V有两种用途,一种是将现有键K的值更新为新值V,另一种是给映射组添加一 个全新的K-V对。这个操作总是成功的。 % 表达式K := V的作用是将现有键K的值更新为新值V。如果被更新的映射组不包含键K,这个 操作就会失败。 F1. %#{a => 1,b => 2} F3 = F1#{ c => 3 }. % #{a => 1,b => 2,c => 3} F4 = F1#{ c := 3 }. % exception error: bad key: c %%% %%使用:=操作符有两个重要原因。首先,如果拼错了新键的名称,我们希望会有错误发生。 如果创建了一个映射组Var = #{keypos => 1, ...},然后用Var #{key_pos := 2 }更新它, 那么几乎可以肯定拼错了键名,而我们需要知道这一点。第二个原因和效率有关。如果在映射组 更新操作里只使用:=操作符,那么我们就知道新旧映射组都带有一组相同的键,因此可以共享相 同的键描述符。假如我们有一个包含数百万映射组的列表,并且它们的各个键都相同,那么所节 省的空间是很可观的。 %%% 使用映射组的最佳方式是在首次定义某个键时总是使用Key => Val,而在修改具体某个键的值时都使用Key := Val。 %%%
-
模式匹配映射组字段
用来编写映射组的=>语法还可以作为映射组模式使用。和之前一样,映射组模式里的键不 能包含任何未绑定变量,但是值现在可以包含未绑定变量了(在模式匹配成功后绑定)。
举例:
Henry8 = #{class => king,born => 1491, died => 1547}. #{born => B} = Henry8. B. #{D => 1547}.
-
操作映射组的内置函数
-
映射组排序
-
以JSON为桥梁