Elixir
提供了丰富的数据类型(实际上只是基本类型的扩展)。前面介绍了元组和列表。他们的特点就是线性结构,通常称之为序列。此外,elixir还有一些哈希类型的结构,这些可以称之为容器集合(collection)结构。下面就来介绍elixir中的一些哈希容器。
图 map
哈希结构是通过一些键值对组合的字典。每一种编程语言中都提供了哈希结构。elixir中更是提供了多种哈希结构。其中Map
就是比较常用的一种。图是由一个键(key)和值(value)为基本存储单元的结构。key和value都可以是任意类型的数据。Map是在Erlang/OTP 17.0之后引入的数据结构。map通常只用来表示元素比较少的情况,当有大量元素的时候,elixir推荐使用HashDict
这个结构(稍后介绍)。
map 的定义
map的定义也比较简单,使用一个%
跟随一堆花括号{}
。键和值之间使用 =>
(通常其他hash结构使用:
, 例如python和js)。例如:
iex(1)> bob = %{:name => "Bob", :age => 25, :work_at => "Initech"}
%{age: 25, name: "Bob", work_at: "Initech"}
定义了一个简单地map,其中键都是Atom
类型,值有字符串和数字。针对这种map,可以使用下面的语法糖简写:
iex(1)> bob = %{:name => "Bob", :age => 25, :work_at => "Initech"}
%{age: 25, name: "Bob", work_at: "Initech"}
iex(2)> bob2 = %{name: "Bob", age: 25, work_at: "Initech"}
%{age: 25, name: "Bob", work_at: "Initech"}
iex(3)> bob === bob2
true
#### map 的读取
读取一个map键的值,使用中括号[]
加上键名的atom即可。也可以使用 .
操作符,前者访问不存在的键的时候,将会返回nil
,后者则会抛出语法错误:
iex(5)> bob[:work_at]
"Initech"
iex(6)> bob[:non_existent_field]
nil
iex(7)> bob.work_at
"Initech"
iex(8)> bob.:non_existent_field
** (SyntaxError) iex:8: syntax error before: non_existent_field
map 的修改
map的修改使用 |
,因为elixir的数据都是不可变的,因此修改将会返回一个新的map。也可以同时修改多个键的值。但是只能修改已经存在的key,否则会报错。之所以这样限制,其目的是为了更有效的提高map更新的效率。因为map结构不会变,那么修改前后各个“版本”的map还将会被引用,可是使用更少的内存,操作也就更加迅速:
iex(8)> next_years_bob = %{bob | age: 26}
%{age: 26, name: "Bob", work_at: "Initech"}
iex(9)> bob
%{age: 25, name: "Bob", work_at: "Initech"}
iex(10)> %{bob | age: 26, work_at: "Initrode"}
%{age: 26, name: "Bob", work_at: "Initrode"}
iex(11)> %{bob | non_existent_feild: 'new'}
** (ArgumentError) argument error
(stdlib) :maps.update(:non_existent_feild, 'new', %{age: 25, name: "Bob", work_at: "Initech"})
(stdlib) erl_eval.erl:255: anonymous fn/2 in :erl_eval.expr/5
(stdlib) lists.erl:1261: :lists.foldl/3
除了使用 |
更新map之外,还可以使用模块的方法,即使用Map
模块和Dict
模块来更新map。并且使用模块的方式可以增加新的key和值, 但是不能同时更新多个值:
iex(13)> Map.put(bob, :salary, 5000)
%{age: 25, name: "Bob", salary: 5000, work_at: "Initech"}
iex(14)> Map.put(bob, :age, 26)
%{age: 26, name: "Bob", work_at: "Initech"}
iex(15)> Map.put(bob, :age, 26, :work_at, "Home")
** (UndefinedFunctionError) undefined function: Map.put/5
(elixir) Map.put(%{age: 25, name: "Bob", work_at: "Initech"}, :age, 26, :work_at, "Home")
iex(15)> Dict.put(bob, :salary, 5000)
%{age: 25, name: "Bob", salary: 5000, work_at: "Initech"}
iex(16)> Dict.put(bob, :age, 26)
%{age: 26, name: "Bob", work_at: "Initech"}
iex(17)> Dict.put(bob, :salary, 5000, :work_at, "Home")
** (UndefinedFunctionError) undefined function: Dict.put/5
(elixir) Dict.put(%{age: 25, name: "Bob", work_at: "Initech"}, :salary, 5000, :work_at, "Home")
通常情况下,使用Map模块要比Dict模块的操作速度更快,不过map用于存储少量的元素。因此,其自身提供的方法可以应付很多应用场景。
map的模式匹配
毫无疑问,elixir中模式匹配无处不在,map当然也可以进行模式匹配。与列表,元组不一样,map的模式匹配,不需要把所有的key和value都写入模式里, 匹配不存在的key会失败,因而会报错:
iex(17)> %{name: real_name} = bob
%{age: 25, name: "Bob", work_at: "Initech"}
iex(18)> real_name
"Bob"
iex(19)> %{name: real_name, age: real_age} = bob
%{age: 25, name: "Bob", work_at: "Initech"}
iex(20)> %{name: real_name, age: real_age, salary: real_salary} = bob
** (MatchError) no match of right hand side value: %{age: 25, name: "Bob", work_at: "Initech"}
关键字列表 Keyword List
定义
map是标准的哈稀结构,这样的键值字典,elixir还有好几种。Keyword算是一种奇怪的哈稀,有其行而无其实。通常定义一个列表使用中括号。定义一个两个元素的元组。如果元祖的第一个元素是一个atom,那么就可以写成下面类似哈稀的结构:
iex(20)> bob = [{:name, "Bob"}, {:age, 25}, {:work_at, "Intiech"}]
[name: "Bob", age: 25, work_at: "Intiech"] # 返回值就是keyword list的字面方式
iex(21)> bob2 = [name: "Bob", age: 25, work_at: Intiech]
[name: "Bob", age: 25, work_at: "Intiech"]
iex(22)> bob === bob2
true
iex(25)> tom = [{25, :age}, {:name, "Tom"}]
[{25, :age}, {:name, "Tom"}]
获取keyword的值
keyword list的读取可以使用 Keyword 模块。通常keyword也是比较小的key-value数据结构:
iex(28)> Keyword.get(days, :monday)
1
iex(29)> Keyword.get(days, :noday)
nil
iex(31)> days[:monday]
1
iex(32)> days[:noday]
nil
iex(33)> days.monday
** (ArgumentError) argument error
:erlang.apply([monday: 1, tuesday: 2, wednesday: 3], :monday, [])
keyword 的读取操作和map及其类似。可是keyword毕竟还是列表,读取某个键的值所消耗的时间复杂度还是O(n)。而map则是O(1)。keyword的应用场景呢?通常用于函数的参数来传递,例如Float.to_string的使用:
iex(35)> Float.to_string(1/3)
"3.33333333333333314830e-01"
iex(36)> Float.to_string(1/3, [decimals: 2])
"0.33"
实际使用中,elixir往往可以让你省略中苦熬和的书写:
iex(37)> Float.to_string(1/3, decimals: 2, compact: true)
"0.33"
iex(38)> Float.to_string(1/3, [decimals: 2, compact: true])
"0.33"
keyword 和 map如此相似,你肯定有疑问到底如何取舍。Elixir中很多函数的可选参数都是keyword,主要原因却有点滑稽。因为Map对于Erlang是比较新的数据结构,在此之前,map所能执行的功能基本都是使用keyword。当然,即使增加了map,两者也不是完全可以替代。比如,keyword允许多个相同的key存在,并且每个key-value是有顺序的。因此实际情况中,他们两者的使用更多的取决于当时你的应用场景。
HashDict
map适合元素比较少的key-value结构。当遭遇大量元素的适合,HashDict将会是很好的帮手。
创建
HashDict是一个模块,用于创建 HashDict 的“实例”结构。使用模块的new
方法可以创建一个空的HashDict结构。
iex(39)> HashDict.new
#HashDict<[]>
为了创建多个元素的HashDict,需要借助Enum模块的into方法:
iex(47)> days = [monday: 1, tuesday: 2, wednesday: 3] |>
...(47)> Enum.into(HashDict.new)
#HashDict<[monday: 1, tuesday: 2, wednesday: 3]>
Enum.into 函数可以将任何可以枚举(enumerable)转换成可以容器化(collectable)的结构。关于可枚举和可容器化将会在协议(protcol)中讨论。此时只需要制知道如何借助其创建一个HashDict。
读写HashDict
获取key的值比较简单,使用模块的get方法接口,当key不存在,会返回nil
iex(48)> HashDict.get(days, :monday)
1
iex(49)> HashDict.get(days, :nodays)
nil
使用 put 方法进行修改和增加新的key-value
iex(51)> HashDict.put(days, :thursday, 4)
#HashDict<[monday: 1, tuesday: 2, wednesday: 3, thursday: 4]>
iex(52)> HashDict.put(days, :tuesday, "two")
#HashDict<[monday: 1, tuesday: "two", wednesday: 3]>
由此可见,类似的容器字典结构,都可以使用 Map,Keyword,HashDict的get方法获取不存在的key的值,使用put方法更新和增加key-value。
HashDict实现了枚举协议,也就是可枚举,当然可以使用Enum的一些方法,比如枚举key-value
iex(54)> days = HashDict.put(days, :thursday, 4)
#HashDict<[monday: 1, tuesday: 2, wednesday: 3, thursday: 4]>
iex(55)> Enum.each(
...(55)> days,
...(55)> fn(key_value) ->
...(55)> key = elem(key_value, 0)
...(55)> value = elem(key_value, 1)
...(55)> IO.puts "#{key} => #{value}"
...(55)> end )
monday => 1
tuesday => 2
wednesday => 3
thursday => 4
:ok
HashDict和map的差别还是比较好区分,HashDict对于大量的元素集合,其性能会比map好,而map却提供了很多操作上语法糖。
无论Map,Keyword,HashDict都是实现字典式的操作。可以发现,他们的模块方法,通常配合这Enum模块进行使用。实际上,Enum模块是Elixir中重要模块,它提供了很多对数据结构操作的高级封装。有些函数直接省去了需要递归实现的循环。
一般的编程语言的介绍,通常流程就是介绍其基本数据结构,然后就介绍一下控制结构。至此,elixir基本数据类型我们都了解了一遍,接下来将会讨论其控制结构。