关键字列表和地图

到目前为止,我们还没有讨论任何关联数据结构,即能够将某个值(或多个值)关联到关键字的数据结构。不同的语言称这些不同的名称,如字典,散列,关联数组等。

在 Elixir 中,我们有两个主要的关联数据结构:关键字列表和地图。现在是了解更多关于它们的时候了!

关键字列表

在许多函数式编程语言中,通常使用一个2元组元组列表作为键值数据结构的表示。在 Elixir 中,当我们有一个元组列表,并且元组的第一项(即关键字)是一个原子时,我们称之为关键字列表:

iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true

正如您在上面看到的那样,Elixir 支持用于定义这些列表的特殊语法:[key: value]。在它下面映射到与上面相同的元组列表。由于关键字列表是列表,我们可以使用列表中可用的所有操作。例如,我们可以使用++将新值添加到关键字列表中:

iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, 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

这些特征是促使关键字列表成为在 Elixir 中将选项传递给函数的默认机制。在第5章中,当我们讨论if/2宏时,我们提到支持以下语法:

iex> if false, do: :this, else: :that
:that

do:else:对是关键字列表!事实上,上述呼吁相当于:

iex> if(false, [do: :this, else: :that])
:that

正如我们上面所看到的,它与以下内容相同:

iex> if(false, [{:do, :this}, {:else, :that}])
:that

通常,当关键字列表是函数的最后一个参数时,方括号是可选的。

虽然我们可以对关键字列表进行模式匹配,但在实践中很少这样做,因为列表上的模式匹配需要项目数及其匹配顺序:

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 中主要使用关键字列表来传递可选值。如果您需要存储许多物品或保证一键通关的最大一值,则应该使用地图。

地图

无论何时您需要键值存储,地图都是 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}

如上所示,只要模式中的键存在于给定的地图中,地图就会匹配。因此,空地图匹配所有地图。

变量可以在访问,匹配和添加地图键时使用:

iex> n = 1
1
iex> map = %{n => :one}
%{1 => :one}
iex> map[n]
:one
iex> %{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}
%{1 => :one, 2 => :two, 3 => :three}

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 = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}

iex> %{map | 2 => "two"}
%{2 => "two", :a => 1}
iex> %{map | :c => 3}
** (KeyError) key :c not found in: %{2 => :b, :a => 1}

上面的语法需要给定的键存在。它不能用于添加新的密钥。例如,在:c密钥中使用它失败,因为:c地图中没有。

当地图中的所有键都是原子时,可以使用关键字语法来方便使用:

iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}

地图另一个有趣的特性是它们提供了自己的访问原子键的语法:

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}

iex> map.a
1
iex> map.c
** (KeyError) key :c not found in: %{2 => :b, :a => 1}

在使用地图时,Elixir 开发人员通常更喜欢使用map.field语法和模式匹配,而不是Map模块中的函数,因为它们导致了自信的编程风格。此博客文章提供了有关如何通过在 Elixir 中编写自定义代码来获得更简洁更快速的软件的见解和示例。

注意:地图最近被引入到Erlang虚拟机中,并且只有从Elixir v1.2开始才能够有效地保存数百万个密钥。因此,如果您正在使用以前的 Elixir 版本(v1.0或v1.1)并且您需要支持至少数百个密钥,则可以考虑使用HashDict模块

嵌套数据结构

通常我们会在地图内部绘制地图,甚至在地图内部绘制关键字列表等等。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"}]

我们有一个用户关键字列表,其中每个值都是包含姓名,年龄和每个用户喜欢的编程语言列表的地图。如果我们想访问约翰的年龄,我们可以写:

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允许动态访问数据结构。查看Kernel模块中的相应文档以获取更多信息

这就结束了我们对Elixir中关联数据结构的介绍。您会发现,给定关键字列表和地图,您将始终拥有正确的工具来解决 Elixir 中需要关联数据结构的问题。