檢視原始碼 清單和元組
在本章節中,我們將學習 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
元組會將元素連續儲存在記憶體中。這表示透過索引存取元組元素或取得元組大小是快速的操作。索引從 0 開始
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
,而且可能會很昂貴,因為它依賴於整個字串的遍歷。
現在我們已經熟悉了這門語言中的基本資料類型,在討論更複雜的資料結構之前,讓我們學習用於撰寫程式碼的重要結構。