关键字列表和地图
到目前为止,我们还没有讨论任何关联数据结构,即能够将某个值(或多个值)关联到关键字的数据结构。不同的语言称这些不同的名称,如字典,散列,关联数组等。
在 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/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"}]
我们有一个用户关键字列表,其中每个值都是包含姓名,年龄和每个用户喜欢的编程语言列表的地图。如果我们想访问约翰的年龄,我们可以写:
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
允许动态访问数据结构。查看Kernel
模块中的相应文档以获取更多信息。
这就结束了我们对Elixir中关联数据结构的介绍。您会发现,给定关键字列表和地图,您将始终拥有正确的工具来解决 Elixir 中需要关联数据结构的问题。