查看源代码 列表和元组
本章将学习 Elixir 中最常用的两种集合数据类型:列表和元组。
(链接) 列表
Elixir 使用方括号来指定一个值列表。值可以是任何类型
iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> length([1, 2, 3])
3
可以使用 ++/2
和 --/2
运算符分别连接或减去两个列表。
iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]
列表运算符永远不会修改现有列表。连接或删除列表中的元素会返回一个新列表。我们说 Elixir 数据结构是 *不可变* 的。不可变性的一个优点是它能导致更清晰的代码。您可以自由地传递数据,并保证没有人会在内存中修改它 - 只是转换它。
在整个教程中,我们会经常谈到列表的头和尾。头是列表的第一个元素,尾是列表的其余部分。它们可以使用函数 hd/1
和 tl/1
来检索。让我们将一个列表分配给一个变量并检索它的头和尾
iex> list = [1, 2, 3]
iex> hd(list)
1
iex> tl(list)
[2, 3]
获取空列表的头或尾会引发错误
iex> hd([])
** (ArgumentError) argument error
有时您会创建一个列表,它会返回一个以 ~c
开头的引用的值。例如
iex> [11, 12, 13]
~c"\v\f\r"
iex> [104, 101, 108, 108, 111]
~c"hello"
当 Elixir 看到一个可打印的 ASCII 数字列表时,Elixir 会将其打印为字符列表(实际上是一个字符列表)。字符列表在与现有 Erlang 代码交互时非常常见。无论何时在 IEx 中看到一个值,并且您不确定它是什么,可以使用 i/1
来检索有关它的信息。
iex> i ~c"hello"
Term
i ~c"hello"
Data type
List
Description
...
Raw representation
[104, 101, 108, 108, 111]
Reference modules
List
Implemented protocols
...
我们将在 “二进制、字符串和字符列表” 章中详细讨论字符列表。
单引号字符串
在 Elixir 中,您也可以使用
'hello'
来构建字符列表,但这在 Elixir v1.15 中被软弃用,并在未来版本中发出警告。最好使用~c"hello"
代替。
元组
Elixir 使用大括号来定义元组。与列表一样,元组可以包含任何值
iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size({:ok, "hello"})
2
元组在内存中连续存储元素。这意味着通过索引访问元组元素或获取元组大小是一个快速操作。索引从零开始
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
iex> tuple_size(tuple)
2
还可以使用 put_elem/3
在元组中的特定索引处放置一个元素
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}
请注意,put_elem/3
返回了一个新的元组。存储在 tuple
变量中的原始元组没有被修改。与列表一样,元组也是不可变的。元组上的每个操作都会返回一个新的元组,它永远不会改变给定的元组。
列表还是元组?
列表和元组有什么区别?
列表在内存中以链表的形式存储,这意味着列表中的每个元素都包含它的值,并指向下一个元素,直到到达列表的末尾。这意味着获取列表的长度是一个线性操作:我们需要遍历整个列表才能确定它的大小。
类似地,列表连接的性能取决于左手边的列表长度
iex> list = [1, 2, 3]
[1, 2, 3]
# This is fast as we only need to traverse `[0]` to prepend to `list`
iex> [0] ++ list
[0, 1, 2, 3]
# This is slow as we need to traverse `list` to append 4
iex> list ++ [4]
[1, 2, 3, 4]
另一方面,元组在内存中连续存储。这意味着获取元组大小或通过索引访问元素速度很快。另一方面,更新或向元组添加元素代价昂贵,因为它需要在内存中创建一个新的元组
iex> tuple = {:a, :b, :c, :d}
{:a, :b, :c, :d}
iex> put_elem(tuple, 2, :e)
{:a, :b, :e, :d}
但是,请注意,元素本身没有被复制。当您更新一个元组时,所有条目在旧元组和新元组之间共享,除了被替换的条目。此规则适用于 Elixir 中的大多数数据结构。这减少了语言需要执行的内存分配量,并且只有在语言的不可变语义下才有可能。
这些性能特征决定了这些数据结构的用法。简而言之,当返回的元素数量可能会有所不同时,使用列表。元组具有固定的大小。让我们看看来自 String
模块的两个示例
iex> String.split("hello world")
["hello", "world"]
iex> String.split("hello beautiful world")
["hello", "beautiful", "world"]
函数 String.split/2
会将字符串在每个空格字符处拆分为一个字符串列表。由于返回的元素数量取决于输入,因此我们使用列表。
另一方面,String.split_at/2
会在给定位置将字符串拆分为两部分。由于它总是返回两个条目,无论输入大小如何,它都会返回元组
iex> String.split_at("hello world", 3)
{"hel", "lo world"}
iex> String.split_at("hello world", -4)
{"hello w", "orld"}
使用元组和原子来创建“标记元组”也很常见,当操作可能成功或失败时,这是一种方便的返回值。例如,File.read/1
会读取给定路径下的文件内容,该文件可能存在也可能不存在。它返回标记元组
iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}
如果给定到 File.read/1
的路径存在,它会返回一个元组,其中第一个元素是原子 :ok
,第二个元素是文件内容。否则,它会返回一个元组,其中包含 :error
和错误描述。正如我们很快将要学习的,Elixir 允许我们在标记元组上进行 *模式匹配*,并轻松地处理成功和失败两种情况。
鉴于 Elixir 一直遵循这些规则,因此在您学习和使用该语言时,列表和元组之间的选择会变得更加清晰。Elixir 通常会引导您做正确的事情。例如,有一个 elem/2
函数用于访问元组项
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
但是,鉴于您通常不知道列表中的元素数量,除了它的头之外,没有用于访问列表中任意条目的内置等效项。
大小还是长度?
当计算数据结构中的元素数量时,Elixir 也遵循一个简单的规则:如果操作是常数时间(值是预先计算的),则函数名为 size
;如果操作是线性时间(计算长度随着输入的增长而变慢),则函数名为 length
。作为助记符,“length”和“linear”都以“l”开头。
例如,到目前为止我们已经使用了 4 个计数函数:byte_size/1
(用于字符串中的字节数)、tuple_size/1
(用于元组大小)、length/1
(用于列表长度)和 String.length/1
(用于字符串中音节的数量)。我们使用 byte_size
来获取字符串中的字节数,这是一个廉价的操作。另一方面,检索 Unicode 音节的数量使用 String.length/1
,并且可能很昂贵,因为它依赖于遍历整个字符串。
现在我们已经熟悉了语言中的基本数据类型,让我们学习编写代码的重要结构,然后我们将讨论更复杂的数据结构。