7-鍵值列表-圖-字典
================
[鍵值列表](#71)
[圖](#72-%E5%9B%BEmaps)
[字典](#73-%E5%AD%97%E5%85%B8dicts)
到目前還沒有講到任何關聯性數據結構,即那種可以將一個或幾個值關聯到一個key上。
不同語言有不同的叫法,如字典,哈希,關聯數組,圖,等等。
Elixir中有兩種主要的關聯性結構:鍵值列表(keyword list)和圖(map)。
## 7.1-鍵值列表
在很多函數式語言中,常用二元元組的列表來表示關聯性數據結構。在Elixir中也是這樣。
當我們有了一個元組(不一定僅有兩個元素的元組)的列表,并且每個元組的第一個元素是個 **原子**,
那就稱之為鍵值列表:
```elixir
iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
iex> list[:a]
1
```
>當原子key和關聯的值之間沒有逗號分隔時,可以把原子的冒號拿到字母的后面。這時,原子與后面的數值之間要有一個空格。
如你所見,Elixir使用比較特殊的語法來定義這樣的列表,但實際上它們會映射到一個元組列表。
因為它們是簡單的列表而已,所有針對列表的操作,鍵值列表也可以用。
比如,可以用```++```運算符為列表添加元素:
```elixir
iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]
```
上面例子中重復出現了```:a```這個key,這是允許的。
以這個key取值時,取回來的是第一個找到的(因為有序):
```elixir
iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0
```
鍵值列表十分重要,它有兩大特點:
- 有序
- key可以重復(!仔細看上面兩個示例)
例如,[Ecto庫](https://github.com/elixir-lang/ecto)使用這兩個特點
寫出了精巧的DSL(用來寫數據庫query):
```elixir
query = from w in Weather,
where: w.prcp > 0,
where: w.temp < 20,
select: w
```
這些特性使得鍵值列表成了Elixir中為函數提供額外選項的默認手段。
在第5章我們討論了```if/2```宏,提到了下方的語法:
```elixir
iex> if false, do: :this, else: :that
:that
```
do: <block>和else: <block> 就是鍵值列表!事實上代碼等同于:
```elixir
iex> if(false, [do: :this, else: :that])
:that
```
當鍵值列表是函數最后一個參數時,方括號就成了可選的。
為了操作關鍵字列表,Elixir提供了
[鍵值(keyword)模塊](http://elixir-lang.org/docs/stable/elixir/Keyword.html)。
記住,鍵值列表就是簡單的列表,和列表一樣提供了線性的性能。
列表越長,獲取長度或找到一個鍵值的速度越慢。
因此,關鍵字列表在Elixir中一般就作為函數調用的可選項。
如果你要存儲大量數據,并且保證一個鍵只對應最多一個值,那就使用圖。
對鍵值列表做模式匹配:
```elixir
iex> [a: a] = [a: 1]
[a: 1]
iex> a
1
iex> [a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
iex> [b: b, a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
```
盡管如此,對列表使用模式匹配很少用到。因為不但要元素個數相等,順序還要匹配。
## 7.2-圖(maps)
無論何時想用鍵-值結構,圖都應該是你的第一選擇。Elixir中,用```%{}```定義圖:
```elixir
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
```
和鍵值列表對比,圖有兩主要區別:
- 圖允許任何類型值作為鍵
- 圖的鍵沒有順序
如果你向圖添加一個已有的鍵,將會覆蓋之前的鍵-值對:
```elixir
iex> %{1 => 1, 1 => 2}
%{1 => 2}
```
如果圖中的鍵都是原子,那么你也可以用鍵值列表中的一些語法:
```elixir
iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}
```
對比鍵值列表,圖的模式匹配很是有用:
```elixir
iex> %{} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}
```
如上所示,圖A與另一個圖B做匹配。
圖B中只要包含有圖A的鍵,那么兩個圖就能匹配上。若圖A是個空的,那么任意圖B都能匹配上。
但是如果圖B里不包含圖A的鍵,那就匹配失敗了。
圖還有個有趣的功能:它提供了特殊的語法來修改和訪問原子鍵:
```elixir
iex> map = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> map.a
1
iex> %{map | :a => 2}
%{:a => 2, 2 => :b}
iex> %{map | :c => 3}
** (ArgumentError) argument error
```
使用上面兩種語法要求的前提是所給的鍵是切實存在的。最后一條語句錯誤的原因就是鍵```:c```不存在。
未來幾章中我們還將討論結構體(structs)。結構體提供了編譯時的保證,它是Elixir多態的基礎。
結構體是基于圖的,上面例子提到的修改鍵值的前提就變得十分重要。
[圖模塊](http://elixir-lang.org/docs/stable/elixir/Map.html)提供了許多關于圖的操作。
它提供了與鍵值列表許多相似的API,因為這兩個數據結構都實現了字典的行為。
>圖是最近連同[EEP 43](http://www.erlang.org/eeps/eep-0043.html)被引入Erlang虛擬機的。
Erlang 17提供了EEP的部分實現,只支持_一小部分_圖功能。
這意味著圖僅在存儲不多的鍵時,圖的性能還行。
為了解決這個問題,Elixir還提供了
[HashDict模塊](http://elixir-lang.org/docs/stable/elixir/HashDict.html)。
該模塊提供了一個字典來支持大量的鍵,并且性能不錯。
## 7.3-字典(Dicts)
Elixir中,鍵值列表和圖都被稱作字典。
換句話說,一個字典就像一個接口(在Elixir中稱之為行為behaviour)。
鍵值列表和圖模塊實現了該接口。
這個接口定義于[Dict模塊](http://elixir-lang.org/docs/stable/elixir/Dict.html),
該模塊還提供了底層實現的一個API:
```elixir
iex> keyword = []
[]
iex> map = %{}
%{}
iex> Dict.put(keyword, :a, 1)
[a: 1]
iex> Dict.put(map, :a, 1)
%{a: 1}
```
字典模塊允許開發者實現自己的字典形式,提供一些特殊的功能。
字典模塊還提供了所有字典類型都可以使用的函數。
如,```Dicr.equal?/2```可以比較兩個字典類型(可以是不同的實現)。
你會疑惑些程序時用keyword,Map還是Dict模塊呢?答案是:看情況。
如果你的代碼期望接受一個關鍵字作為參數,那么使用簡直列表模塊。
如果你想操作一個圖,那就使用圖模塊。
如果你想你的API對所有字典類型的實現都有用,
那就使用字典模塊(確保以不同的實現作為參數測試一下)。