7 关键字,表单和字典

迄今我们还没有讨论过相关性的数据结构,即那些把一个值(或多个值)和一个键(key)相关起来的数据结构。不同的语言对这些数据结构有不同的称呼,字典,哈希,相关性数组,表单等等。

在Elixir中,我们有两种主要的相关性数据结构:关键字列表和表单。是时候揭开它们的真面目了。

7.1 关键字列表

在许多的计算机语言中,经常见到用一个2元的元组来表示某种相关性的数据结构。在Elixir里,当我们有一个元组的列表并且每个元组的第一个元素是一个原子的时候,我们称为关键字列表:

  1. iex> list = [{:a, 1}, {:b, 2}]
  2. [a: 1, b: 2]
  3. iex> list == [a: 1, b: 2]
  4. true
  5. iex> list[:a]
  6. 1

正如你在上面列子里看到的,Elixir支持一种特殊的语法来定义这样一种列表,不过在底层它还是映射到一个元组列表的。因为它们本身就是列表,所以所有能用在列表上的函数,也能应用在它们身上,包括它们的性能特性也和列表相近。

例如,我们能用++把一个新的值加入到一个关键字列表中:

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

注意排在前面的元素在查找的时候,会被有限选取:

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

关键字列表是非常重要的,因为它们有两个特性:

  • 键的顺序有开发者控制
  • 允许给一个键复制多次

例如,Ecto利用了上面的两个特性来提供了一种优雅的DSL来编写数据库queries:

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

这些特性促使关键字列表称为了Elixir中给函数传递参数的默认机制。在第五章,当我们讨论宏if/2,我们提到如下的语法是合法的:

  1. iex> if false, do: :this, else: :that
  2. :that

do:else:启示就是关键字列表!实际上,这个和上面的调用是等价的:

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

总体来讲,当函数的最后一个参数是一个关键字列表的时候,方括号就不是必须的。

为了能处理关键字列表,Elixir提供了Keyword模块。记住虽然关键字列表本质上还是列表,它们的性能如果列表也是线性的。列表越长,在计算长度之类的操作时, 就需要更多的时间来找到某个键。因此,关键字列表在Elixir中主要被用于可选项。如果你需要存储很多元素或确定哪一个键的值是最大的,有应该用表单(map)替代。

注意我们同样能对关键字列表进行模式匹配:

  1. iex> [a: a] = [a: 1]
  2. [a: 1]
  3. iex> a
  4. 1
  5. iex> [a: a] = [a: 1, b: 2]
  6. ** (MatchError) no match of right hand side value: [a: 1, b: 2]
  7. iex> [b: b, a: a] = [a: 1, b: 2]
  8. ** (MatchError) no match of right hand side value: [a: 1, b: 2]

不过在实际中这很少用到,因为这需要列表的大小和元素的顺位都同时匹配。

7.2 表单(Maps)

在Elixir中,无论何时你需要存储一个键-值对,表单都是最佳的数据结构选择。创建一个表单需要用%{}语法:

  1. iex> map = %{:a => 1, 2 => :b}
  2. %{2 => :b, :a => 1}
  3. iex> map[:a]
  4. 1
  5. iex> map[2]
  6. :b

和关键字列表比价,我们可以清楚地看到不同:

  • 表单允许任何类型的键
  • 表单的键没有特定的顺序

当你创建表单的时候,如果有重复的键,只有最后一个才会被保留:

  1. iex> %{1 => 1, 1 => 2}
  2. %{1 => 2}

当一个表单中的所有键都是原子的时候,你就可以用关键字语法了:

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

和关键字列表不同,表单在模式匹配中非常有用:

  1. iex> %{} = %{:a => 1, 2 => :b}
  2. %{:a => 1, 2 => :b}
  3. iex> %{:a => a} = %{:a => 1, 2 => :b}
  4. %{:a => 1, 2 => :b}
  5. iex> a
  6. 1
  7. iex> %{:c => c} = %{:a => 1, 2 => :b}
  8. ** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}

正如上面的例子显示的,只要表单里有那个键,模式就能一直匹配。也就是说,一个空表单匹配所有的其他表单。

表单的一个有意思的特点是它提供了一个特有的语法来更新和访问原子类的键:

  1. iex> map = %{:a => 1, 2 => :b}
  2. %{:a => 1, 2 => :b}
  3. iex> map.a
  4. 1
  5. iex> %{map | :a => 2}
  6. %{:a => 2, 2 => :b}
  7. iex> %{map | :c => 3}
  8. ** (ArgumentError) argument error

在上面的例子中,更新和访问都需要特定的键的存在。例如,最后一行之所以失败是因为在表单中没有键:c。这一点对当你只期望表单中只包含特定的几个键时将会非常有用。

在未来的篇章中,我们将会学到结构(structs),它为Elixir的多态性提供了一个编译时的保证和基础。结构就是构件在表单之上的,上面例子中显示的表单的更新的确定性会证明是非常有用的。

对表单的处理是用过Map模块中的函数完成的,它提供了一个同关键字列表非常相似的API。这是因为表单和关键字列表都实现和字典(dict)行为。

表单是新近才通过EEP 443被引入到Erlang虚拟机里的。Erlang 17提供了一个EEP的部分实现,它只实现了所谓的“小表单”。这意味着现在的表单只有当存储的元素数量不太大(几十个)的时候性能才有保证。为了弥补这一点,Elixir提供了HashDict模块,这个模块用一个哈希算法提供了一个支持存储几十万计键而又能保证良好性能的字典。

7.3 字典(Dicts)

在Elixir中,关键字列表和表单都是字典。也就是说,一个字典就如同一个界面(interface)(在Elixir中称为行为,behaviours),关键字列表和表单都实现了这个界面。

这个界面是定义在Dict模块之中,而且也它也提供了一套需要具体实现的API:

  1. iex> keyword = []
  2. []
  3. iex> map = %{}
  4. %{}
  5. iex> Dict.put(keyword, :a, 1)
  6. [a: 1]
  7. iex> Dict.put(map, :a, 1)
  8. %{a: 1}

字典模块允许开发者实现它们自己的字典,拥有自己的特性,并且能在现有的Elixir代码中使用。字典模块也提供了一些在所有的字典实现中都同用的函数。比如,函数Dict.equal?/2就能比较两个不同类型的字典。

看到这里,你可能有些迷糊了,到底该如何选择使用KeywordMap还是Dict?答案是依情况而定。

如果你的代码期望把关键字当成函数参数,那就直接用Keyword模块。如果你想处理表单,就用Map模块。然而,如果你的API期望能够使用任何类型的字典,那就只能用Dict模块了(别忘了写测试,并且确保不同类型的字典实现都能通过测试)。

以上就是我们对Elixir中相关性数据结构的一些结论。你将会发现有了关键字列表和表单,面对任何有关需要相关性数据结构的问题时候,你都能得心应手的工具。