檢視原始碼 關鍵字清單和映射

現在讓我們來談論關聯資料結構。關聯資料結構能夠將一個金鑰關聯到一個特定值。不同的語言會用不同的名稱來稱呼這些資料結構,例如字典、雜湊、關聯陣列等。

在 Elixir 中,我們有兩個主要的關聯資料結構:關鍵字清單和映射。

關鍵字清單

關鍵字清單是一種用於傳遞選項給函式的資料結構。想像你想要分割一個數字字串。我們可以使用 String.split/2

iex> String.split("1 2 3", " ")
["1", "2", "3"]

然而,如果數字之間有額外的空格會發生什麼事

iex> String.split("1  2  3", " ")
["1", "", "2", "", "3"]

正如你所見,我們的結果中現在有空字串。幸運的是,String.split/3 函式允許將 trim 選項設定為 true

iex> String.split("1  2  3", " ", [trim: true])
["1", "2", "3"]

[trim: true] 是關鍵字清單。此外,當關鍵字清單是函式的最後一個參數時,我們可以省略括號並寫成

iex> String.split("1  2  3", " ", trim: true)
["1", "2", "3"]

如上例所示,關鍵字清單主要用作函式的選用參數。

顧名思義,關鍵字清單就是清單。特別是,它們是由 2 項元組組成的清單,其中第一個元素(金鑰)是原子,而第二個元素可以是任何值。這兩種表示方式是相同的

iex> [{:trim, true}] == [trim: true]
true

由於關鍵字清單是清單,因此我們可以使用清單中所有可用的操作。例如,我們可以使用 ++ 來將新值新增到關鍵字清單

iex> list = [a: 1, b: 2]
[a: 1, b: 2]
iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]

你可以使用括號語法來讀取關鍵字清單的值。這也稱為存取語法,因為它是由 Access 模組定義的

iex> list[:a]
1
iex> list[:b]
2

如果金鑰重複,則會擷取新增到前面的值

iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0

關鍵字清單很重要,因為它們有三個特殊特性

  • 鍵必須是原子。
  • 鍵是有序的,由開發人員指定。
  • 鍵可以給予多次。

例如,Ecto 函式庫利用這些功能提供一個優雅的 DSL 來撰寫資料庫查詢

query =
  from w in Weather,
    where: w.prcp > 0,
    where: w.temp < 20,
    select: w

雖然我們可以在關鍵字清單上進行模式配對,但實際上並不會這麼做,因為在清單上進行模式配對需要項目數量和它們的順序相符

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]

此外,由於關鍵字清單通常用作選用參數,因此它們用於可能並非所有鍵都存在的情況,這將使得無法在它們上進行配對。簡而言之,不要在關鍵字清單上進行模式配對。

為了處理關鍵字清單,Elixir 提供 Keyword 模組。但請記住,關鍵字清單只是清單,因此它們提供與清單相同的線性效能特性:清單越長,尋找鍵、計算項目數量等所花費的時間就越長。如果您需要將大量鍵儲存在鍵值資料結構中,Elixir 提供映射,我們很快就會學習到。

do 區塊和關鍵字

正如我們所見,關鍵字主要用於語言中傳遞選用值。事實上,我們在此指南中之前已經使用過關鍵字。例如,我們已經看過

iex> if true do
...>   "This will be seen"
...> else
...>   "This won't"
...> end
"This will be seen"

碰巧 do 區塊只不過是建立在關鍵字之上的語法便利性。我們可以將上述重寫為

iex> if true, do: "This will be seen", else: "This won't"
"This will be seen"

仔細注意兩種語法。在關鍵字清單格式中,我們使用逗號分隔每個鍵值對,每個鍵後面跟著 :。在 do 區塊中,我們擺脫了冒號、逗號,並用換行符號分隔每個關鍵字。它們之所以有用,正是因為它們在撰寫程式碼區塊時消除了冗長性。大多數時候,您將使用區塊語法,但知道它們是等效的會很好。

這在語言中扮演著重要的角色,因為它允許 Elixir 語法保持簡潔但仍然具有表達力。我們只需要少數資料結構來表示語言,當討論 選用語法 以及深入探討 元程式設計 時,我們將回頭探討這個主題。

在了解完這些之後,讓我們來談談映射。

映射作為鍵值對

每當你需要儲存鍵值對時,映射是 Elixir 中的「首選」資料結構。使用 %{} 語法建立映射

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
iex> map[:c]
nil

與關鍵字清單相比,我們已經可以看到兩個差異

  • 映射允許任何值作為鍵。
  • 映射的鍵不會遵循任何順序。

與關鍵字清單相反,映射在模式配對中非常有用。當映射用於模式中時,它將始終與給定值的子集相匹配

iex> %{} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}

如上所示,只要模式中的鍵存在於給定的映射中,映射就會匹配。因此,空映射與所有映射相匹配。

Map 模組提供與 Keyword 模組非常相似的 API,具有新增、移除和更新映射鍵的便利功能

iex> Map.get(%{:a => 1, 2 => :b}, :a)
1
iex> Map.put(%{:a => 1, 2 => :b}, :c, 3)
%{2 => :b, :a => 1, :c => 3}
iex> Map.to_list(%{:a => 1, 2 => :b})
[{2, :b}, {:a, 1}]

預定義鍵的映射

在前一節中,我們已將映射用作鍵值資料結構,其中可以隨時新增或移除鍵。但是,建立具有預定義鍵集的映射也很常見。它們的值可能會更新,但永遠不會新增或移除新鍵。當我們知道我們正在處理的資料的形狀時,這很有用,如果我們得到不同的鍵,則很可能是其他地方出錯了。

我們使用與前一節中相同的語法來定義此類映射,但所有鍵都必須是原子

iex> map = %{:name => "John", :age => 23}
%{name: "John", age: 23}

從上面列印的結果中可以看到,Elixir 還允許你使用與關鍵字清單相同的 key: value 語法來寫入原子鍵的映射。

當鍵是原子時,特別是在處理預定義鍵的映射時,我們也可以使用 map.key 語法來存取它們

iex> map = %{name: "John", age: 23}
%{name: "John", age: 23}

iex> map.name
"John"
iex> map.agee
** (KeyError) key :agee not found in: %{name: "John", age: 23}

還有一個用於更新鍵的語法,如果鍵尚未定義,它也會引發錯誤

iex> %{map | name: "Mary"}
%{name: "Mary", age: 23}
iex> %{map | agee: 27}
** (KeyError) key :agee not found in: %{name: "John", age: 23}

這些操作有一個很大的好處,那就是如果鍵不存在於映射中,它們會引發錯誤,編譯器甚至可以在可能的情況下偵測並發出警告。這使得它們對於快速獲得回饋並及早發現錯誤和拼寫錯誤很有用。這也是為另一項 Elixir 功能「結構」提供支援的語法,我們稍後會學習。

在處理映射時,Elixir 開發人員通常更喜歡使用 map.key 語法和模式配對,而不是 Map 模組中的函式,因為它們會導致一種自信的程式設計風格。 José Valim 的這篇部落格文章 提供了見解和範例,說明你如何透過在 Elixir 中寫入自信的程式碼來獲得更簡潔、更快速的軟體。

巢狀資料結構

通常我們會在映射中放入映射,甚至在映射中放入關鍵字清單,依此類推。Elixir 提供便利功能,透過 put_in/2update_in/2 和其他巨集來處理巢狀資料結構,提供與你在命令式語言中會發現的相同便利性,同時保留語言的不變屬性。

想像您有下列結構

iex> users = [
  john: %{name: "John", age: 27, languages: ["Erlang", "Ruby", "Elixir"]},
  mary: %{name: "Mary", age: 29, languages: ["Elixir", "F#", "Clojure"]}
]
[
  john: %{age: 27, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
  mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}
]

我們有一個使用者關鍵字清單,每個值都是一個包含名稱、年齡和每個使用者喜歡的程式語言清單的地圖。如果我們想要存取 John 的年齡,我們可以寫

iex> users[:john].age
27

我們也可以用這個相同的語法來更新值

iex> users = put_in users[:john].age, 31
[
  john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
  mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}
]

update_in/2 巨集很類似,但允許我們傳遞一個控制值如何變化的函式。例如,我們從 Mary 的語言清單中移除「Clojure」

iex> users = update_in users[:mary].languages, fn languages -> List.delete(languages, "Clojure") end
[
  john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
  mary: %{age: 29, languages: ["Elixir", "F#"], name: "Mary"}
]

還有更多關於 put_in/2update_in/2 的內容需要學習,包括 get_and_update_in/2,它允許我們一次提取值和更新資料結構。還有 put_in/3update_in/3get_and_update_in/3,它們允許動態存取資料結構。

摘要

在 Elixir 中有兩種不同的資料結構可用於處理鍵值儲存。除了 Access 模組和模式比對之外,它們還提供了一組豐富的工具,用於處理複雜的、潛在巢狀的資料結構。

在我們結束本章時,重要的是要記住您應該

  • 使用關鍵字清單將選用值傳遞給函式

  • 使用地圖作為一般鍵值資料結構

  • 在處理具有預定義鍵集的資料時使用地圖

現在讓我們來談談模組和函式。