现在让我们来谈谈关联数据结构。关联数据结构能够将键与某个值关联起来。不同的语言对这些结构有不同的称呼,如字典、哈希、关联数组等。
在 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
关键字列表很重要,因为它们具有三个特殊特征:
1.键必须是原子。
2.键是按开发人员指定的顺序排列的。
3.键可以多次使用。
例如,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 提供了关键字模块。但请记住,关键字列表只是列表,因此它们提供与它们相同的线性性能特征:列表越长,查找键、计算项目数量等所需的时间就越长。如果您需要在键值数据结构中存储大量键,Elixir 提供了映射,我们很快就会学到。
do-blocks 和关键字
正如我们所见,关键字在语言中主要用于传递可选值。事实上,我们之前在本指南中已经使用过关键字。例如,我们已经看到:
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-blocks 中,我们删除了冒号和逗号,并用换行符分隔每个关键字。它们之所以有用,正是因为它们在编写代码块时消除了冗长。大多数时候,您会使用块语法,但最好知道它们是等效的。
这在语言中起着重要作用,因为它允许 Elixir 语法保持简洁但仍然富有表现力。我们只需要几个数据结构来表示语言,这个话题我们会在讨论可选语法时回顾,并在讨论元编程时深入探讨。
说完这些,我们来谈谈映射。
映射作为键值对
每当您需要存储键值对时,映射都是 Elixir 中的"首选"数据结构。使用 %{} 语法创建映射:
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
iex> map[:c]
nil
与关键字列表相比,我们已经看到两个区别:
1.映射允许任何值作为键。
2.映射的键不遵循任何顺序。
与关键字列表相比,映射在模式匹配方面非常有用。当在模式中使用映射时,它将始终匹配给定值的子集:
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 另一项功能"Structs"的语法,我们将在后面学习。
Elixir 开发人员在使用映射时通常更喜欢使用 map.key 语法和模式匹配,而不是 Map 模块中的函数,因为它们可以实现一种断言式的编程风格。José Valim 的这篇博文提供了一些见解和示例,说明如何通过在 Elixir 中编写断言式代码来获得更简洁、更快速的软件。
嵌套数据结构
我们通常会在映射内有映射,甚至在映射内有关键字列表,等等。Elixir 通过 put_in/2、update_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/2 和 update_in/2 还有更多需要学习的内容,包括 get_and_update_in/2,它允许我们提取值并立即更新数据结构。还有 put_in/3、update_in/3 和 get_and_update_in/3,它们允许动态访问数据结构。
摘要
Elixir 中有两种不同的数据结构可用于处理键值存储。除了 Access 模块和模式匹配之外,它们还提供了一套丰富的工具来处理复杂的、可能嵌套的数据结构。
在结束本章时,需要牢记的是您应该:
1.使用关键字列表将可选值传递给函数
2.使用映射来处理一般的键值数据结构
3.使用映射来处理具有预定义键集的数据
现在让我们来谈谈模块和函数。