Erlang笔记 -- 常用数据结构

本文详细介绍了Erlang中的数据结构,包括元组、列表、记录、映射组、进程字典以及ETS和DETS。讨论了如何创建、操作和使用这些数据结构,特别是提取和更新元素的方法,以及映射组和ETS表的高级功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

元组tuple

元组,用于保存固定数量的元素。创建元组的方法就是用大括号{}把想要表达的值括起来,并用逗号分隔他们。例如,表示某人的年龄:{Jack, 18}
Erlang没有类型声明,创建一个坐标点,只需:

P = {10, 15}.

这样就创建了一个元祖并绑定在变量P上。
由于元组里的字段没有名字,为了记住该元组的用途,通常会写成以下方式:{point, 10, 15},这样提高使程序的可理解性。
元组是可以嵌套的,例如:

1> Person = {person, {name, Jack}, {age, 18}}.
{person, {name, Jack}, {age, 18}}
1. 创建元组

元组会在声明他们的时候自动创建,不再使用时被销毁。如果在构建新的元组时用到变量,那么新的元组会共享该变量所引用的数据结构的值。例如,

2> F = {firstname, jack}.
{firstname, jack}
3> L = {lastname, chan}.
{lastname, chan}
4> p = {person, F, L}.
{person, {firstname, jack}, {lastname, chan}}
2. 提取元组的值

通过使用模式匹配操作符=从元组中提取一些值。

1> Point = {point,10,15}.
{point,10,15}
2> {point,X,Y} = Point.
{point,10,15}
3> X.
10
4> Y.
15

注意:等号两侧的元组必须要有相同数量的元素,而且两侧的对应元素必须绑定为相同的值。如:

5> Point1 = {point,5,5}.
{point,5,5}
6> {point,C,C} = Point1.
{point,5,5}
7> C.
5

如果有一个复杂的元组,就可以编写一个与该元组形状(结构)相同的模式,并在待提取值的位置加入未绑定的变量来提取该值。

Eshell V8.3  (abort with ^G)
1> Person = {person,{name,jack,chan},{age,18}}.
{person,{name,jack,chan},{age,18}}
2> {_, {_, Who, _}, _} = Person.
{person,{name,jack,chan},{age,18}}
3> Who.
jack

其中,"_“被称为匿名变量。与正规变量不同,同一模式里的多个”_"不必绑定相同的值。

列表list

列表,被用来存放任意数量的事物。创建列表的方法是用中括号[]把列表元素括起来,并用逗号分隔开。
列表的第一个元素称为列表头head,其余部分称为列表尾tail。例如:列表[1, 2, 3, 4, 5]的列表头为整数1,[2, 3, 4, 5]为列表尾。

1. 定义列表

如果T是一个列表,那么[H | T]也是一个列表,他的头是H,尾是T。竖线 | 把列表的头与尾分隔开。[]是一个空列表。
无论何时,只要用[... | T]表示一个列表,就应该确保T是列表。此时称为格式正确的列表,否则称为不正确的列表。
可以给T的开头添加不止一个元素,写法是[E1,E2,...,EN | T]

1> T = [{apple, 10}, {pear, 5}].
[{apple, 10}, {pear, 5}]
2> T1 = [{orange, 15} | T].
[{orange, 15}, {apple, 10}, {pear, 5}]
2. 提取列表元素

通过模式匹配操作来提取某个列表里的元素。如果有一个列表L,那么表达式[X | Y] = L会提取列表头作为X,列表尾作为Y

3> [B1 | B2] = T1.
[{orange, 15}, {apple, 10}, {pear, 5}]
4> B1.
{orange, 15}
5> B2.
[{apple, 10}, {pear, 5}]
3. 列表处理注意点
Eshell V8.3  (abort with ^G)
1> A = [[1,2],[2,3],[3,4]].
[[1,2],[2,3],[3,4]]
2> [B,C|T] = A.
[[1,2],[2,3],[3,4]]
3> B.
[1,2]
4> C.
[2,3]
5> T.
[[3,4]]
6> D = [1,2,3].
[1,2,3]
7> E = [3,4,5,6].
[3,4,5,6]
8> F = [D|E].
[[1,2,3],3,4,5,6]
9> G = D ++ E.
[1,2,3,3,4,5,6]
4. 列表推导

[F(X) || X <- L]标记表示“由F(X)组成的列表(X从列表L中提取)”。[2 *X || X <- L]表示“由2 * X组成的列表”。
以下实现将列表中每一项的数字加倍:

1> Buy = [{apples,1},{pears,2},{newspaper,1}].
[{apples,1},{pears,2},{newspaper,1}]
2> [{Name,2*Num} || {Name,Num} <- Buy].
[{apples,2},{pears,4},{newspaper,2}]

注意,|| 右侧的元组{Name, Number}是一个模式,用于匹配Buy中的各个元素。左侧的元组{Name, 2*Number}是一个构造器

列表推导最常规的形式是下面这种表达式:

[X || Qualifier1, Qualifier2, ...]

X是任意一条表达式,后面的限定符(Qualifier)可以是生成器、位串生成器或过滤器。

  • 生成器(generator)的写法是Pattern <- ListExpr,其中的ListExp必须是一个能够得出列表的表达式。
  • 位串(bitstring)生成器的写法是BitStringPattern <= BitStringExpr,其中的BitStringExpr必须是一个能够得出位串的表达式。
  • 过滤器(filter)既可以是判断函数(即返回truefalse的函数),也可以是布尔表达式。
5. 列表常用函数
  • lists:keyfind(Key, N, TupleList) -> Tuple | false
    在元组列表TupleList中搜索第n个元素与Key相等的元组。如果找到这样的元组,则返回元组,否则返回false

  • lists:keystore(Key, N, TupleList1, NewTuple) -> TupleList2
    返回TupleList1的副本,其中第n个元素等于Key的元组T的第一个出现位置将被替换为NewTuple,如果没有这样的元组T,则返回TupleList1的副本,并将[NewTuple]附加到末尾。

  • lists:keysort(N, TupleList1) -> TupleList2
    返回一个包含列表TupleList1的已排序元素的列表。排序是在元组的第n个元素上执行的。

  • lists:sort(Fun, List1) -> List2
    根据排序函数Fun,返回一个包含List1排序元素的列表。

  • lists:usort(List1) -> List2
    返回一个包含List1的已排序元素的列表,并删除重复项。

  • lists:keymember(Key, N, TupleList) -> boolean()
    如果TupleList中有一个元组,其第n个元素与Key相等,则返回true,否则返回false

  • lists:member(Elem, List) -> boolean()
    如果Elem在列表List中,则返回true,否则返回false

  • lists:sublist(List1, Len) -> List2
    返回List1的子列表,从位置1开始,包含Len个元素。当Len超过列表的长度时,将返回整个列表。

  • lists:sublist(List1, Start, Len) -> List2
    返回List1的子列表,从位置Start开始,包含Len个元素。当Start+Len超过列表的长度时,将返回整个列表。

  • lists:foldl(Fun, Acc0, List) -> Acc1
    对列表的连续元素Elem调用Fun(Elem,AccIn),从AccIn==Acc0开始。Fun/2必须返回一个新的累加器,它被传递给下一个调用。函数返回累加器的最终值。如果列表为空,则返回Acc0

  • lists:foldr(Fun, Acc0, List) -> Acc1
    同foldl/3,但遍历元素顺序为从右到左。如下:

> P = fun(A, AccIn) -> io:format("~p ", [A]), AccIn end.
#Fun<erl_eval.12.2225172>
> lists:foldl(P, void, [1,2,3]).
1 2 3 void
> lists:foldr(P, void, [1,2,3]).
3 2 1 void
  • lists:any(Pred, List) -> boolean()
    Pred = fun((Elem :: T) -> boolean())
    List = [T]
    T = term()
    如果列表中至少一个元素Elem使得Pred(Elem)返回true,则返回true

  • lists:all(Pred, List) -> boolean()
    如果列表中所有元素Elem都使得Pred(Elem)返回true,则返回true

  • lists:nthtail(N, list) -> Tail
    返回从N+1开始一直到列表的结尾的子列表。

  • lists:nth(N, List) -> Elem
    返回列表的第n个元素。

  • erlang:hd(List)
    返回列表的头,即第一个元素。

  • lists:last(List) -> Elem
    返回列表中的最后一个元素。

  • lists:flatmap(Fun, List1) -> List2
    //Fun = fun((A) -> [B])
    //List1 = [A]
    //List2 = [B]
    //A = B = term()
    Takes a function from As to lists of Bs, and a list of As (List1) and produces a list of Bs by applying the function to every element in List1 and appending the resulting lists.
    That is, flatmap behaves as if it had been defined as follows:

flatmap(Fun, List1) -> append(map(Fun, List1)).

Example:

> lists:flatmap(fun(X)->[X,X] end, [a,b,c]).
[a,a,b,b,c,c]

记录record

记录是元组的另一种形式,通过使用记录,可以给元组里的各个元素关联一个名称。
用记录声明命名元组里的元素,语法如下:

-record(Name, {
	%%以下两个键带有默认值
	key1 = Default1,
	key2 = Default2,
	...
	%%下一行就相当于Key3 = Undefined
	Key3,
	...     
}).

-record不是一个shell命令,在shell里要使用rr。记录声明只能在Erlang源代码模块里使用,不能用于shell。
Name是记录名。Key1Key2这些是记录中各个字段的名称,他们必须是原子。记录里的每个字段都可以带一个默认值,如果创建记录时没有指定某个字段值,就会使用默认值。
记录的定义既可以保存在Erlang源代码文件里,也可以由扩展名为.hrl的文件保存,然后包含在Erlang源代码文件里。
文件包含是唯一能确保多个Erlang模块共享相同记录定义的方式。

%% records.hrl
-record(todo, {status=reminder, who=jack, text}).

记录一旦定义,就可以创建该记录的实例了。

要在shell中创建实例,必须先把记录的定义读入shell。

1> rr("record.hrl").
[todo]

rr是read records的缩写,即读取记录

1. 创建记录
2> #todo{}.
#todo{status = reminder,who = jack,text = undefined}
3> X1 = #todo{status = urgent, text = "So urgent"}.
#todo{status = urgent,who = jack,text = "So urgent"}
4> X2 = X1#todo{status = done}.
#todo{status = done,who = jack,text = "So urgent"}

语法#todo{key1=Val1,... ,keyn=Valn}用于创建一个类型为todo的新记录。所有键都是原子,而且必须与记录定义里所用一致。如果省略一个键,将会用记录定义里的值作为该键的默认值。
第4行复制了一个现有的记录。语法X1#todo{status=done}的意思是创建一个X1的副本(类型必须为todo),并修改字段status的值为done。此操作不会改变原始记录。

2. 提取记录的值

若想一次性提取记录的多个字段,可使用模式匹配:

5> #todo{who = Who, text = Text} = X2.
#todo{status = done,who = jack,text = "So urgent"}
6> Who.
jack
7> Text.
"So urgent"

若只想提取单个字段,可使用"点语法":

8> X2#todo.text.
"So urgent"
3. 在函数里模式匹配记录

我们可以编写模式匹配记录字段或创建记录的函数,代码如下:

clear_status(#todo{status=S, who=W} = R) ->
	%%在此函数中,S和W绑定了记录里的字段值
	%%
	%%R是*整个*记录
	R#todo{status=done}
	%%...

要匹配某个类型的记录,可以这样编写函数:

do_sth(X) when is_record(X, todo) ->
%%...

这个子句会在Xtodo类型的记录时匹配成功。

4. 记录是元组的另一种形式
9> X2.
#todo{status = done,who = jack,text = "So urgent"}
10> rf(todo).
ok
11> X2.
{todo,done,jack,"So urgent"}

rf(todo)命令使shell忘了todo的定义,再次打印X2时则显示成了元组。

12> rr("record.hrl").
[todo]
13> X2.
#todo{status = done,who = jack,text = "So urgent"}

再次读入定义后,打印X2则显示记录。

无法添加记录定义中未定义的字段:

14> X3 = X2#todo{number = 1}.
* 1: field number undefined in record todo
15> X2 = X2#todo{status = undone}.
** exception error: no match of right hand side value
                    #todo{status = undone,who = jack,text = "So urgent"}

也无法直接更新某个变量对应的记录的字段值,本质上因为Erlang中的变量一旦绑定后是不允许改变的。

5. 以下原子不能作为记录的字段名
L = ['after','begin','case','try','cond','catch','andalso','orelse','end','fun',
   'if','let','of','query','receive','when','bnot','not','div','rem','band','and',
   'bor','bxor','bsl','bsr','or','xor'].

映射组maps

映射组,是键-值对的关联性集合。键可以是任何的Erlang数据类型。
映射组的语法与记录相似,不同之处在于省略了记录名,并且键-值分隔符是=>:=

#{Key1 Op Val1, Key2 Op Val2, ... , KeyN Op ValN}
1. 创建映射组

键和值可以是任何有效的Erlang数据类型。

1> F1 = #{a => 1, b => 2}.
#{a => 1,b => 2}
2> Facts = #{{wife, fred} => "Sue", {age, fred} => 45, {daughter, fred} => "Mary"}.
#{{age,fred} => 45,
  {daughter,fred} => "Mary",
  {wife,fred} => "Sue"}

映射在系统内部是作为有序集和存储的,打印时总是按键排序后的顺序,与映射组的创建方式无关。

3> F2 = #{b => 2, a => 1}.
#{a => 1,b => 2}
4> F1 = F2.
#{a => 1,b => 2}
2. 更新映射组

要基于现有的映射组更新一个映射组,语法如下:

NewMap = OldMap#{Key1 Op Val1, Key2 Op Val2, ... , KeyN Op ValN}

表达式Key => Val有两种用途,一种是将现有的键Key的只更新为Val;另一种是给映射添加一个全新的Key-Val对。这个操作总是成功的。
表达式Key := Val的作用是将现有键Key的值更新为Val。如果Key不存在时,则操作失败。

5> F3 = F1#{c => xx}.
#{a => 1,b => 2,c => xx}
6> F4 = F1#{c := 3}.
** exception error: {badkey,c}
     in function  maps:update/3
        called as maps:update(c,3,#{a => 1,b => 2})
     in call from erl_eval:'-expr/5-fun-0-'/2 (erl_eval.erl, line 255)
     in call from lists:foldl/3 (lists.erl, line 1263)

使用映射组的最佳方式是,在首次定义某个键时总是使用Key => Val,而在修改具体某个键的值时用Key := Val

3. 模式匹配映射组字段

用来更新映射组的:=语法还可以作为映射组模式使用。
映射组模式里的键不能包含任何未绑定变量,但值可以包含未绑定变量。

1> Henry8 = #{class => king, born => 1491, died => 1547}.
#{born => 1491,class => king,died => 1547}
2> #{born := Born} = Henry8.
#{born => 1491,class => king,died => 1547}
3> Born.
1491
4> #{D => 1547}.
* 1: variable 'D' is unbound
5> #{D := 1547}.
* 1: only association operators '=>' are allowed in map construction
6> H1 = Henry8#{D => 123}.
* 1: variable 'D' is unbound
7> H1 = Henry8#{died => 123}.
#{born => 1491,class => king,died => 123}
8> H2 = Henry8#{died := 123}.
#{born => 1491,class => king,died => 123}
9> Henry8.
#{born => 1491,class => king,died => 1547}

可以在函数的头部使用包含模式的映射组,前提是映射组里所有的键都是已知的。
定义一个count_characters(Str)函数,让它返回一个映射组,内含某个字符串里各个字符的出现次数。

count_character(Str) ->
	count_character(Str, #{}).

count_character( [H | T], #{ H := N } = X ) ->
	count_character(T, X#{ H := N+1 });
count_character( [H | T], X ) ->
	count_character(T, X#{ H => 1});
count_character( [], X ) -> 
	X.
1> count_character("hello").
#{101=>1, 104=>1, 108=>2, 111=>1}

其中,101是eASCII码。

4. 操作映射组的内置函数
  • maps:new() -> #{}
    返回一个新的空映射组。

  • erlang:is_map(M) -> boolean()
    判断M是否为映射组,是则返回true,否则返回false。可用于关卡测试或函数主体中。

  • maps:to_list(M) -> [{K1,V1}, …, {Kn,Vn}]
    将映射组M转换为一个键值列表,严格按升序排列

> Map = #{42 => value_three,1337 => "value two","a" => 1}.
> maps:to_list(Map).
[{42,value_three},{1337,"value two"},{"a",1}]
  • maps:from_list([K1, V2], … , [Kn, Vn]) -> M
    把包含键值对的列表转化为映射组M。如果同样的键出现不止一次,就是用最后一次出现所关联的值,前面的值被忽略。
> List = [{"a",ignored},{1337,"value two"},{42,value_three},{"a",1}].
> maps:from_list(List).
#{42 => value_three,1337 => "value two","a" => 1}
  • maps:size(M) -> NumberOfEntries
    返回映射组里的条目数量

  • maps:is_key(Key, M) -> boolean()
    如果Key是M中的一个键,那么返回true。否则为false。

  • maps:get(Key, Map) -> Val
    返回映射组Map中与Key关联的值

  • maps:find(Key, Map) -> {ok, Val} | error
    返回映射组Map中与Key关联的值,否则返回error

  • maps:keys(Map) -> [Key1, … ,Keyn]
    返回映射组所含的键的列表,按升序排列

  • maps:remove(Key, M) -> M1
    返回一个新映射M1,除了键位Key的项被移除外,其他的与M一致。

  • maps:without( [Key1, … , Keyn], M ) -> M1
    返回一个新映射M1,它是M的复制,但移除了带有[Key1, … , Keyn]列表内这些键的元素。

  • maps:difference(M1, M2) -> M3
    M3是M1的复制,但移除了那些与M2里的元素具有相同键的元素。等价于:maps:without(maps:keys(M2), M1)

  • maps:fold(Fun, Init, MapOrIter) -> Acc
    函数 F(K, V, AccIn) 递归调用映射组Map里的每一个键和值,函数 F/3肯定会返回一个新的上一次函数成功执行累积值,该函数返回最终的累积值。如果映射组Map是一个空的映射组,那么初始的累积值Init将会返回。

Fun = fun(K,V,AccIn) when is_list(K) -> AccIn + V end,
Map = #{"k1" => 1, "k2" => 2, "k3" => 3},
maps:fold(Fun, 1, Map).
%% Result: 7
  • maps:values(Map) -> Values
    返回映射组所含的值的列表,按升序排列,当Map不是映射组时,抛出{badmap,Map}异常。
5. 映射组排序
  • 首先当maps:size(A) > maps:size(B)时,A > B
  • 若相等,则当maps:to_list(A) > maps:to_list(B)时,A > B
    例如,A = #{age => 23, person => "jim"} < B = #{email => "sue@123.com", name => "sue"},因为A的最小键age小于B的最小键email

进程字典process_dict

每个Erlang进程都有一个被称为进程字典(process dictionary)的私有数据存储区域。进程字典是一个关联数组,它由若干个键和值组成。每个键只有一个值。
这个字典可以用下列内置函数进行操作:

  • put(Key, Value) -> OldValue
    给进程字典添加一个Key, Value组合。put的返回值是OldValue,也就是Key之前关联的值。如果没有之前的值,就返回原子undefined
  • get(Key) -> Value
    查找Key的值。如果字典里存在Key, Value组合就返回Value,否则返回原子undefined
  • get() -> [{Key, Value}]
    返回整个字典,形式是一个由{Key,Value}元组所构成的列表。
  • get_keys(Value) -> [Key]
    返回一个列表,内含字典里所有值为Value的键。
  • erase(Key) -> Value
    返回Key的关联值,如果不存在则返回原子undefined。最后,删除Key的关联值。
  • erase() -> [{Key, Value}]
    删除整个进程字典。返回值是一个由{Key,Value}元组所构成的列表,代表了字典删除之前的状态。

举栗:

1> put(a,1).
undefined
2> put(b,2).
undefined
3> get(a).
1
4> get().
[{b,2},{a,1}]
5> get_keys(1).
[a]
6> erase().
[{b,2},{a,1}]

ets和dets

ETSErlang Term Storage(Erlang数据存储)的缩写,DETS则是Disk ETS(磁盘ETS)的缩写。
ETSDETS执行的任务基本相同:它们提供大型的键-值查询表。ETS常驻内存,DETS则常驻磁盘。

ETS是相当高效的:可以用它存储海量的数据(只要有足够的内存),执行查找的时间也是恒定的(在某些情况下是对数时间)。DETS提供了几乎和ETS一样的接口,但它会把表保存在磁盘上。因为DETS使用磁盘存储,所以它远远慢于ETS,但是运行时的内存占用也会小很多。另外,ETSDETS表可以被多个进程共享,这就让跨进程的公共数据访问变得非常高效。

ETSDETS表其实就是Erlang元组的集合。

ETS表里的数据保存在内存里,它们是易失的。当ETS表被丢弃或者控制它的Erlang进程终止时,这些数据就会被删除。保存在DETS表里的数据是非易失的,即使整个系统崩溃也能留存下来。DETS表在打开时会进行一致性检查,如果发现有损坏,系统就会尝试修复它。

1. 表的类型

ETSDETS表保存的是元组。元组里的某一个元素(默认是第一个)被称为该表的键。通过键来向表里插入和提取元组。
一些表被称为异键表(set),它们要求表里所有的键都是唯一的。
另一些被称为同键表(bag),它们允许多个元素拥有相同的键。
基本的表类型(异键表和同键表)各有两个变种,它们共同构成四种表类型:
异键 → 各个元组里的键都必须是独一无二的
有序异键(ordered set)→ 元组会被排序
同键 → 可以有不止一个元组拥有相同的键,但是不能有两个完全相同的元组。
副本同键(duplicate bag)→ 可以有多个元组拥有相同的键,而且在同一张表里可以存在多个相同的元组。

2. ETS和DETS表有四种基本操作
  • 创建一个新表或打开现有的表
    ets:newdets:open_file 实现。
  • 向表里插入一个或多个元组
    这里要调用 insert(TableId, X) ,其中 X 是一个元组或元组列表。 insert 在ETS和DETS里有着相同的参数和工作方式。
  • 在表里查找某个元组
    这里要调用 lookup(TableID, Key) 。得到的结果是一个匹配 Key 的元组列表。 lookup在ETS和DETS里都有定义。
    lookup的返回值始终是一个元组列表,这样就能对异键表和同键表使用同一个查找函数。
    如果表的类型是同键,那么多个元组可以拥有相同的键。如果表的类型是异键,那么查找成功后的列表里只会有一个元素。
    如果表里没有任何元组拥有所需的键,就会返回一个空列表。
  • 丢弃某个表
    用完某个表后可以告知系统,方法是调用dets:close(TableId)ets:delete(TableId)
%% ets_test.erl
-module(ets_test).
-export([start/0]).

start() ->
	lists:foreach(fun test_ets/1, [set, ordered_set, bag, duplicate_bag]).

test_ets(Mode) ->
	TableId = ets:new(test, [Mode]),
	ets:insert(TableId, {a, 1}),
	ets:insert(TableId, {b, 2}),
	ets:insert(TableId, {a, 1}),
	ets:insert(TableId, {a, 3}),
	List = ets:tab2list(TableId),
	io:format("~-13w => ~p~n", [Mode, List]),
	ets:delete(TableId).

这个程序会为四种模式各创建一个ETS表,然后向表内插入 {a,1}{b,2}{a,1}{a,3} 。然后调用 tab2list ,它会把整个表转换成一个列表并打印出来。

1> ets_test:start().
set           => [{b,2},{a,3}]
ordered_set   => [{a,3},{b,2}]
bag           => [{b,2},{a,1},{a,3}]
duplicate_bag => [{b,2},{a,1},{a,1},{a,3}]
ok

在异键型的表里,每个键都只能出现一次。如果先向表内插入元组 {a,1} 然后再插入 {a,3} ,那么最终值将是 {a,3}

3. 影响 ETS 表效率的因素

ETS表在内部是用散列表表示的(除了有序异键表,它是用平衡二叉树表示的)。这就意味着使用异键表会带来少许空间开销,而使用有序异键表会带来时间开销。插入异键表所需的时间是恒定的,而插入有序异键表所需的时间与表内条目数量的对数成比例。
使用同键表的代价比使用副本同键表更高,因为每次插入时都需要与所有拥有相同键的元素比较是否相等。如果有大量元组都拥有同一个键,这么做就会很低效。
ETS表保存在一个单独的存储区域里,与正常的进程内存无关。可以认为ETS表归创建它的进程所有,当这个进程挂了或者调用 ets:delete 时,表就会被删除。ETS表不会进行垃圾收集,这就意味着即使表里存储了海量的数据也不会产生垃圾收集的开销。
当一个元组被插入ETS表时,所有代表这个元组的数据结构都会从进程的栈复制到ETS表里。当你对表执行查询操作时,找到的元组会从ETS表复制到进程的栈上。
所有的数据结构都是如此,除了大型的二进制数据。这些二进制型会存储在它们自己的堆外存储区域里。这个区域可以被多个进程和ETS表共享,各个二进制型则由一个引用计数垃圾收集器管理,它会记录有多少个不同的进程和ETS表在使用这个二进制型。如果某个特定二进制型的进程和表使用计数降至零,那么这个二进制型的存储区域就可以被回收。

4. 创建一个 ETS 表

创建ETS表的方法是调用 ets:new 。创建表的进程被称为该表的主管。创建表时所设置的一组选项在以后是无法更改的。如果主管进程挂了,表的空间就会被自动释放。你可以调用ets:delete 来删除表。
ets:new 的参数如下:

-spec ets:new(Name, [Opt]) -> TableId

其中, Name 是一个原子。 [Opt]是一列选项,源于下面这些参数。

  • set | ordered_set | bag | duplicate_bag
    创建一个指定类型的ETS表(我们之前讨论过)。
  • private
    创建一个私有表,只有主管进程才能读取和写入它。
  • public
    创建一个公共表,任何知道此表标识符的进程都能读取和写入它。
  • protected
    创建一个受保护表,任何知道此表标识符的进程都能读取它,但只有主管进程才能写入它。
  • named_table
    如果设置了此选项, Name就可以被用于后续的表操作。
  • {keypos, K}
    K作为键的位置。通常键的位置是1。基本上唯一需要使用这个选项的场合是保存Erlang记录(它其实是元组的另一种形式),并且记录的第一个元素包含记录名的时候。
    注意:不使用任何选项打开一个ETS表就相当于用[set,protected,{keypos,1}]选项打开它。
5. 保存元组到磁盘

ETS表把元组保存在内存里,而DETS提供了把Erlang元组保存到磁盘上的方法。DETS的最大文件大小是2GB
DETS文件必须先打开才能使用,用完后还应该正确关闭。如果没有正确关闭,它们就会在下次打开时自动进行修复。因为修复可能会花很长一段时间,所以先正确关闭它们再结束程序是很重要的。
DETS表有着和ETS表不同的共享属性。DETS表在打开时必须赋予一个全局名称。如果两个或更多本地进程用相同的名称和选项打开某个DETS表,它们就会共享这个表。这个表会一直处于打开状态,直到所有进程都关闭它(或者崩溃)。

6. 范例:文件名索引

创建一个基于磁盘的表,它将把文件名映射到整数上,反之亦然。我们将定义函数filename2index 和它的逆函数 index2filename
为了实现这个索引,我们将创建一个DETS表,然后用三种不同类型的元组填充它。

  • {free, N}
    N是表里的第一个空白索引。当我们在表里输入一个新文件名时,就会指派索引N给它。
  • {FileNameBin, K}
    FileNameBin(一个二进制型)被指派了索引K
  • {K, FileNameBin}
    K(一个整数)代表文件FilenameBin
    请注意,每个新添加的文件都会在表里增加两个条目:一个File → Index条目和一个逆向的Index → Filename条目。这是出于效率的原因。当ETSDETS表建立时,元组里只有一个元素充当键的角色。匹配某个非键元组元素虽然可行,但非常低效,因为它涉及对整个表进行搜索。当整个表都在磁盘上时,这个操作的代价尤其高昂。
    现在来编写这个程序。首先编写打开和关闭DETS表的函数,这个表用来保存所有的文件名。
%% lib_filenames_dets.erl
-module(lib_filenames_dets).
-export([
	open/1,
	close/0,
	test/0,
	filename2index/1,
	index2filename/1
]).

open(File) ->
	io:format("dets opened:~p~n", [File]),
	Bool = filelib:is_file(File),
	case dets:open_file(?MODULE, [{file, File}]) of
		{ok, ?MODULE} ->
			case Bool of
				true -> void;
				false -> ok = dets:insert(?MODULE, {free, 1})
			end,
			true;
		{error, Reason} ->
			io:format("cannot open dets table~n"),
			exit({eDetsOpen, File, Reason})
	end.
	
close() ->
	dets:close(?MODULE).
	
filename2index(FileName) when is_binary(FileName) ->
	case dets:lookup(?MODULE, FileName) of
		[] ->
			[{_, Free}] = dets:lookup(?MODULE, free),
			ok = dets:insert(?MODULE, [{free, Free+1}, {Free, FileName}, {FileName, Free}]),
			Free;
		[{_, Free}] ->
			Free
	end;
filename2index(FileName) ->
	io:format("filename2index arg:~p wrong~n", [FileName]).
	
index2filename(Index) when is_integer(Index) ->
	case dets:lookup(?MODULE, Index) of
		[] -> error;
		[{_, Bin}] -> Bin
	end;
index2filename(Index) ->
	io:format("index2filename arg:~p wrong~n", [Index]).
	
test() ->
	open(test_dets),
	Index = filename2index(list_to_binary("test")),
	io:format("The Index is ~p~n", [Index]),
	Filename = index2filename(Index),
	io:format("The Filename is ~p~n", [binary_to_list(Filename)]),
	close().

open 的代码会自动初始化DETS表,方法是在创建新表时插入 {free, 1} 元组。如果 File 存在, filelib:is_file(File) 就会返回 true ,否则返回 false 。请注意, dets:open_file 或者创建一个新文件,或者打开一个现有文件,这就是必须在调用 dets:open_file 前先检查文件是否存在的原因。
我 们 在 这 段 代 码 里 多 次 使 用 了 ?MODULE 宏 , 它 会 展 开 成 当 前 的 模 块 名 ( 即lib_filenames_dets )。许多针对DETS的函数调用需要一个唯一的原子参数作为表名。单凭模块名就能生成一个唯一的表名。因为系统里不能有两个名称相同的Erlang模块,所以如果处处遵循这一惯例,就有理由认定我们有一个可以用于表名的唯一名称。
我每次都用 ?MODULE 宏而不是显式指定模块名,这是因为我有一个习惯,就是会在编写代码的过程中修改模块名。用了宏之后,即使修改了模块名,代码也会是正确的。
打开文件之后,向表里插入一个新文件名就很简单了。它是由 filename2index 调用产生的副作用实现的。如果文件名在表里,就会返回它的索引,否则就会生成一个新索引并更新表,而这次更新会使用三个元组。

filename2index(FileName) when is_binary(FileName) ->
	case dets:lookup(?MODULE, FileName) of
		[] ->
			[{_, Free}] = dets:lookup(?MODULE, free),
			ok = dets:insert(?MODULE, [{free, Free+1}, {Free, FileName}, {FileName, Free}]),
			Free;
		[{_, Free}] ->
			Free
	end;
filename2index(FileName) ->
	io:format("filename2index arg:~p wrong~n", [FileName]).

dets:insert 的第二个参数既可以是一个元组,也可以是一个元组列表。出于效率的原因,文件名用一个二进制型表示。
filename2index 里有一处潜在的竞争状况。如果在 dets:insert尚未被调用时有两个并行进程调用了 dets:lookupfilename2index 就会返回一个不正确的值。为了让这个函数能正常工作,必须确保任一时刻都只能有一个进程调用它。
把索引转换成文件名非常简单。

index2filename(Index) when is_integer(Index) ->
	case dets:lookup(?MODULE, Index) of
		[] -> error;
		[{_, Bin}] -> Bin
	end;
index2filename(Index) ->
	io:format("index2filename arg:~p wrong~n", [Index]).

执行test()如下:

1> lib_filenames_dets:test().
dets opened:test_dets
The Index is 1
The Filename is "test"
ok
7. ets常用接口
  • ets:match/2
    match方法进行匹配最简单,'$数字'代表占位符,'_'代表通配符,'$数字'这种表示方式,数字相对大小代表相对位置顺序;
Eshell V8.3  (abort with ^G)
1> ets:new(countries, [bag, named_table]).
countries
2> ets:insert(countries, {yves, france, cook}).
true
3> ets:insert(countries, {sean, ireland, bartender}).
true
4> ets:insert(countries, {marco, italy, cook}).
true
5> ets:insert(countries, {chris, ireland, tester}).
true
6> ets:match(countries, {'$1', '_', '_'}).
[[sean],[marco],[yves],[chris]]
7> ets:match(countries, {'$1', '$0', '_'}).
[[ireland,sean],[italy,marco],[france,yves],[ireland,chris]]
8> ets:match(countries, {'$11', '$9', '_'}).
[[ireland,sean],[italy,marco],[france,yves],[ireland,chris]]
9> ets:match(countries, {'$11', '$99', '_'}).
[[sean,ireland],[marco,italy],[yves,france],[chris,ireland]]
10> ets:match(countries, {'$101', '$9', '_'}).
[[ireland,sean],[italy,marco],[france,yves],[ireland,chris]]
11> ets:match(countries, {'$2', ireland, '_'}).
[[sean],[chris]]
12> ets:match(countries, {'_', ireland, '_'}).
[[],[]]
13> ets:match(countries, {'$2', cook, '_'}).
[]
14> ets:match(countries, {'$0', '$1', cook}).
[[marco,italy],[yves,france]]
15> ets:match(countries, {'$0', '$0', cook}).
[]
16> ets:match(countries, {'$1', '$0', cook}).
[[italy,marco],[france,yves]]

%%如果没有数字占位符,是没有结果输出的,只是空列表

如果是需要所有字段,提取整个数据项,那就直接使用match_object

17> ets:match_object(countries, {'_', ireland, '_'}).
[{sean,ireland,bartender},{chris,ireland,tester}]

上面的例子countries这个结构很简单,但是如果是一个字段稍多写的结构呢?很容易出现类似ets:match(zen_ets, {'$1','_','_','_','_','_' })。这样的代码,不仅可读性差,而且一旦字段顺序发生变化,这里就容易出错。使用record可以规避掉tuple字段增减,顺序的问题。

  • ets:select/2
    对 ETS 表里的数据进行匹配比对
    用法:
select(Tab, MatchSpec) -> [Match]

使用一个匹配描述从表 Tab 里匹配对象。此函数调用比 ets:match/2ets:match_object/2 更常用。以下是一些最简单形式的匹配描述:

MatchSpec = [MatchFunction]
MatchFunction = {MatchHead, [Guard], [Result]}
MatchHead = "Pattern as in ets:match"
Guard = {"Guardtest name", ...}
Result = "Term construct"

这意味着匹配描述总是含有一个以上元组元素的列表(含有三个参数的元组元素),元组的第一个元素应是 ets:match/2 的文档中所描述的模式,第二个元素应是含 0 个或多个断言测试的列表,第三个元素应是包含关于实际返回值的描述的列表,通常是一个对返回值全描述的列表,即返回匹配对象的所有项目。
返回值的结构使用 MatchHead 所绑定的 “match variables”,或者使用特殊的匹配值 '$_'(整个匹配对象)和 '$$'(包含所有匹配值的列表)。

Eshell V8.3  (abort with ^G)
1> Tab = ets:new(test_ets, [private]).
16400
2> ets:insert(Tab, [{a,1},{b,2}]).
true
3> ets:select(Tab, [{{'$1', '$2'}, [], ['$$']}]).
[[b,2],[a,1]]
4> ets:select(Tab, [{{'$1', '$2'}, [], ['$_']}]).
[{b,2},{a,1}]
5> ets:select(Tab, [{{'$1', '$2'}, [], ['$1']}]).
[b,a]
6> ets:select(Tab, [{{'$1', '$2'}, [], ['$2']}]).
[2,1]
7> ets:select(Tab, [{{'$1', '$2'}, [], ['$1', '$2']}]).
[2,1]

字典dict

  • dict:new() -> dict()
    创建字典

  • dict:size(Dict) -> integer() >= 0
    获取字典大小

  • dict:store(Key, Value, Dict1) -> Dict2
    在字典Dict1中存储Key-Value。如果Dict1中已经存在Key,则将相关联的值替换为Value

  • dict:to_list(Dict) -> List
    Dict转化为列表[{Key, Value},...]

  • dict:from_list(List) -> Dict
    将列表[{Key, Value},...]转化为Dict

  • dict:update(Key, Fun, Dict1) -> Dict2
    通过调用Fun来更新字典中的值以获取新值。如果字典中不存在Key,则生成异常。

  • dict:update(Key, Fun, Initial, Dict1) -> Dict2
    通过调用Fun来更新字典中的值以获取新值。如果字典中不存在Key,则将Initial存储为第一个值。

  • dict:update_counter(Key, Increment, Dict1) -> Dict2
    向与Key关联的Value增加Increment并存储结果。如果Key不在字典中,Increment将作为第一个值存储。

update_counter(Key, Incr, D) ->
    update(Key, fun (Old) -> Old + Incr end, Incr, D).
  • dict:append(Key, Value, Dict1) -> Dict2
    Value追加到与Key关联的值的列表中。

  • dict:append_list(Key, ValList, Dict1) -> Dict2
    将列表ValList追加到与Key关联的列表。如果与Key关联的初始值不是列表,则会生成异常。

  • dict:erase(Key, Dict1) -> Dict2
    从字典中删除Key所对应得项。

  • dict:fetch(Key, Dict) -> Value
    返回Dict中与Key关联的值。如果Key不在字典中,则生成异常。

  • dict:fetch_keys(Dict) -> Keys
    返回Dict中所有Key组成的列表。

  • dict:take(Key, Dict) -> {Value, Dict1} | error
    返回字典中Key所关联的Value,和一个没有此值的新字典。如果字典中不存在Key,则返回error

  • dict:filter(Pred, Dict1) -> Dict2
    Dict2Dict1中所有Key-Value满足Pred(Key,Value)true的字典。

  • dict:find(Key, Dict) -> {ok, Value} | error
    Dict中搜索Key,返回{ok,Value},其中Value是与key关联的值。如果字典中不存在Key,则返回error

  • dict:fold(Fun, Acc0, Dict) -> Acc1
    Fun = fun((Key, Value, AccIn) -> AccOut)

  • dict:is_empty(Dict) -> boolean()
    如果Dict为空则返回true,否则返回false

  • dict:is_key(Key, Dict) -> boolean()
    如果Key存在于Dict中则返回true,否则返回false

  • dict:map(Fun, Dict1) -> Dict2
    Fun = fun((Key, Value1) -> Value2)
    对于Dict1中的每一个Key-Value都执行Fun得到一个Dict2

  • dict:merge(Fun, Dict1, Dict2) -> Dict3
    Fun = fun((Key, Value1, Value2) -> Value)
    合并Dict1Dict2

merge(Fun, D1, D2) ->
    fold(fun (K, V1, D) ->
                 update(K, fun (V2) -> Fun(K, V1, V2) end, V1, D)
         end, D2, D1).
Eshell V8.3  (abort with ^G)
1> D0 = dict:new().
{dict,0,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]}}}
2> D1 = dict:store(files, [], D0).
{dict,1,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],[],[],[[files]],[],[],[],[],[],[],[],[],[],[],[],[]}}}
3> D2 = dict:append(files, f1, D1).
{dict,1,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],[],[],
        [[files,f1]],
        [],[],[],[],[],[],[],[],[],[],[],[]}}}
4> D3 = dict:append(files, f2, D2).
{dict,1,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],[],[],
        [[files,f1,f2]],
        [],[],[],[],[],[],[],[],[],[],[],[]}}}
5> D4 = dict:append(files, f3, D3).
{dict,1,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],[],[],
        [[files,f1,f2,f3]],
        [],[],[],[],[],[],[],[],[],[],[],[]}}}
6> dict:fetch(files, D4).
[f1,f2,f3]
7> D5 = dict:store(a, 1, D4).
{dict,2,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],
        [[a|1]],
        [],
        [[files,f1,f2,f3]],
        [],[],[],[],[],[],[],[],[],[],[],[]}}}
8> D6 = dict:store(a, 11, D5).
{dict,2,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],
        [[a|11]],
        [],
        [[files,f1,f2,f3]],
        [],[],[],[],[],[],[],[],[],[],[],[]}}}
9> dict:find(a, D6).
{ok,11}
10> {Value, D7} = dict:take(files, D6). // OTP20.0才有该方法。
** exception error: undefined function dict:take/2
11> dict:fetch(files, D6).
[f1,f2,f3]
12> dict:fetch_keys(D6).
[a,files]

Erlang中还提供了其他如queuearray等数据结构使用的模块与接口,在此不一一赘述啦,有需要可以看下文档

注:本博客为通过《Erlang程序设计 第二版》学习Erlang时所做的笔记。学习更详细的内容,建议直接阅读《Erlang程序设计 第二版》。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值