檢視原始碼 二進制、字串和字元清單
在 「基本類型」 中,我們學習了關於字串的一些知識,並且使用 is_binary/1
函式進行檢查
iex> string = "hello"
"hello"
iex> is_binary(string)
true
在本章中,我們將更清楚地了解二進制到底是什麼、它們與字串的關聯,以及 Elixir 中的單引號值,'like this'
,是什麼意思。儘管字串是電腦語言中最常見的資料類型之一,但它們卻微妙地複雜,而且經常被誤解。為了了解 Elixir 中的字串,我們必須了解 Unicode 和字元編碼,特別是 UTF-8 編碼。
Unicode 和碼點
為了促進跨多種語言的電腦之間有意義的溝通,需要一個標準,以便一台機器上的 1 和 0 在傳輸到另一台機器時具有相同的意義。 Unicode 標準 充當我們所知幾乎所有字元的官方註冊表:這包括古典和歷史文本、表情符號以及格式化和控制字元。
Unicode 將其曲目中的所有字元組織成碼表,並且每個字元都會得到一個唯一的數字索引。此數字索引稱為 碼點。
在 Elixir 中,你可以在字元文字之前使用 ?
來顯示其碼點
iex> ?a
97
iex> ?ł
322
請注意,大多數 Unicode 碼表會透過其十六進位 (hex) 表示法來參照碼點,例如 97
轉換為十六進位的 0061
,而且我們可以使用 \uXXXX
符號和其碼點數字的十六進位表示法來表示 Elixir 字串中的任何 Unicode 字元
iex> "\u0061" == "a"
true
iex> 0x0061 = 97 = ?a
97
十六進位表示法也可以幫助你查詢有關碼點的資訊,例如 https://codepoints.net/U+0061 有一個關於小寫 a
(又稱碼點 97)的資料表。
UTF-8 和編碼
現在我們了解了 Unicode 標準是什麼,以及什麼是碼點,我們終於可以討論編碼了。碼點是我們儲存的內容,而編碼處理的是我們如何儲存它:編碼是一種實作。換句話說,我們需要一種機制將碼點數字轉換為位元組,以便它們可以儲存在記憶體中,寫入磁碟等。
Elixir 使用 UTF-8 編碼其字串,這表示碼點編碼為一系列 8 位元組。UTF-8 是一種可變寬度字元編碼,使用一到四個位元組儲存每個碼點。它能夠編碼所有有效的 Unicode 碼點。我們來看一個範例
iex> string = "héllo"
"héllo"
iex> String.length(string)
5
iex> byte_size(string)
6
儘管上面的字串有 5 個字元,但它使用了 6 個位元組,因為兩個位元組用於表示字元 é
。
注意:如果您在 Windows 上執行,您的終端機預設可能不使用 UTF-8。您可以在輸入
iex
(iex.bat
) 之前執行chcp 65001
來變更目前階段的編碼。
除了定義字元之外,UTF-8 還提供了音節的概念。音節可能包含多個通常被視為一個的字元。例如,女消防員表情符號表示為三個字元的組合:女人表情符號 (👩)、隱藏的零寬度連接符和消防車表情符號 (🚒)
iex> String.codepoints("👩🚒")
["👩", "", "🚒"]
iex> String.graphemes("👩🚒")
["👩🚒"]
但是,Elixir 足夠聰明,知道它們被視為一個字元,因此長度仍然是一個
iex> String.length("👩🚒")
1
注意:如果您在終端機中看不到上面的表情符號,您需要確保您的終端機支援表情符號,並且您使用的是可以呈現它們的字型。
儘管這些規則聽起來很複雜,但 UTF-8 編碼的文件無處不在。此頁面本身使用 UTF-8 編碼。編碼資訊會提供給您的瀏覽器,然後瀏覽器就會知道如何適當地呈現所有位元組、字元和音節。
如果您想查看字串在檔案中儲存的確切位元組,一個常見的技巧是將空位元組 <<0>>
串聯到它
iex> "hełło" <> <<0>>
<<104, 101, 197, 130, 197, 130, 111, 0>>
或者,你可以使用 IO.inspect/2
來檢視字串的二進制表示。
iex> IO.inspect("hełło", binaries: :as_binaries)
<<104, 101, 197, 130, 197, 130, 111>>
我們有點超前了。讓我們來談談位元串,以了解 <<>>
建構函式的確切含義。
位元串
儘管我們已經介紹過碼點和 UTF-8 編碼,但我們仍然需要更深入地探討我們如何精確地儲存編碼的位元組,而這正是我們引入位元串的地方。位元串是 Elixir 中的基本資料類型,表示為 <<>>/1
語法。位元串是記憶體中連續的位元序列。
預設情況下,8 位元(即 1 位元組)用於儲存位元串中的每個數字,但你可以透過 ::n
修飾詞手動指定位元數,以表示 n
位元的位元大小,或者你可以使用更詳細的宣告 ::size(n)
iex> <<42>> == <<42::8>>
true
iex> <<3::4>>
<<3::size(4)>>
例如,十進制數字 3
在以 2 為底的 4 位元表示時將為 0011
,這等於值 0
、0
、1
、1
,每個值都使用 1 位元儲存
iex> <<0::1, 0::1, 1::1, 1::1>> == <<3::4>>
true
任何超過已配置位元數所能儲存的值都會被截斷
iex> <<1>> == <<257>>
true
在此,257 在 2 為底時將表示為 100000001
,但由於我們只保留了 8 位元來表示它(預設),最左邊的位元會被忽略,而值會被截斷為 00000001
,或十進制的 1
。
二進制
二進制是位元數可以被 8 除盡的位元串。這表示每個二進制都是位元串,但並非每個位元串都是二進制。我們可以使用 is_bitstring/1
和 is_binary/1
函式來示範這一點。
iex> is_bitstring(<<3::4>>)
true
iex> is_binary(<<3::4>>)
false
iex> is_bitstring(<<0, 255, 42>>)
true
iex> is_binary(<<0, 255, 42>>)
true
iex> is_binary(<<42::16>>)
true
我們可以在二進制 / 位元串上進行模式比對
iex> <<0, 1, x>> = <<0, 1, 2>>
<<0, 1, 2>>
iex> x
2
iex> <<0, 1, x>> = <<0, 1, 2, 3>>
** (MatchError) no match of right hand side value: <<0, 1, 2, 3>>
請注意,除非你明確使用 ::
修飾詞,否則預期二進制模式中的每個條目都與一個位元組(正好 8 位元)相符。如果我們想比對未知大小的二進制,我們可以在模式的結尾使用 binary
修飾詞
iex> <<0, 1, x::binary>> = <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> x
<<2, 3>>
在對二進制進行模式比對時,還有其他幾個修飾詞可能很有用。 binary-size(n)
修飾詞將比對二進制中的 n
個位元組
iex> <<head::binary-size(2), rest::binary>> = <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> head
<<0, 1>>
iex> rest
<<2, 3>>
字串是 UTF-8 編碼的二進制,其中每個字元的碼點使用 1 到 4 個位元組編碼。因此,每個字串都是二進制,但由於 UTF-8 標準編碼規則,並非每個二進制都是有效的字串。
iex> is_binary("hello")
true
iex> is_binary(<<239, 191, 19>>)
true
iex> String.valid?(<<239, 191, 19>>)
false
字串串接運算子 <>
實際上是一個二進位串接運算子
iex> "a" <> "ha"
"aha"
iex> <<0, 1>> <> <<2, 3>>
<<0, 1, 2, 3>>
由於字串是二進位,我們也可以對字串進行模式配對
iex> <<head, rest::binary>> = "banana"
"banana"
iex> head == ?b
true
iex> rest
"anana"
不過,請記住二進位模式配對作用於位元組,所以對像「über」這樣的多位元組字元進行配對時,不會配對到字元,而是會配對到該字元的頭一個位元組
iex> "ü" <> <<0>>
<<195, 188, 0>>
iex> <<x, rest::binary>> = "über"
"über"
iex> x == ?ü
false
iex> rest
<<188, 98, 101, 114>>
在上面,x
只配對到多位元組 ü
字元的頭一個位元組。
因此,在對字串進行模式配對時,重要的是使用 utf8
修飾詞
iex> <<x::utf8, rest::binary>> = "über"
"über"
iex> x == ?ü
true
iex> rest
"ber"
字元清單
我們對位元串、二進位和字串的介紹即將完成,但我們還有一個資料類型要說明:字元清單。
字元清單是一個整數清單,其中所有整數都是有效的碼點。實際上,你不會經常遇到它們,只會在特定情況下遇到,例如與不接受二進位作為引數的舊 Erlang 函式庫介面。
iex> ~c"hello"
~c"hello"
iex> [?h, ?e, ?l, ?l, ?o]
~c"hello"
~c
sigil(我們將在 「Sigils」 章節中介紹 sigil)表示我們處理的是字元清單,而不是常規字串。
字元清單不包含位元組,而是包含整數碼點。但是,只有當所有碼點都在 ASCII 範圍內時,清單才會以 sigil 的形式列印
iex> ~c"hełło"
[104, 101, 322, 322, 111]
iex> is_list(~c"hełło")
true
這樣做是為了簡化與 Erlang 的互操作性,即使它可能會導致一些令人驚訝的行為。例如,如果你儲存的整數清單恰好介於 0 到 127 之間,預設情況下,IEx 會將其解釋為字元清單,並顯示對應的 ASCII 字元。
iex> heartbeats_per_minute = [99, 97, 116]
~c"cat"
你可以隨時透過呼叫 inspect/2
函式,強制將字元清單列印為其清單表示形式
iex> inspect(heartbeats_per_minute, charlists: :as_list)
"[99, 97, 116]"
此外,你可以使用 to_string/1
和 to_charlist/1
將字元清單轉換為字串,反之亦然。
iex> to_charlist("hełło")
[104, 101, 322, 322, 111]
iex> to_string(~c"hełło")
"hełło"
iex> to_string(:hello)
"hello"
iex> to_string(1)
"1"
上述函式是多態的,換句話說,它們接受多種形式:它們不僅可以將字元清單轉換為字串(反之亦然),還可以轉換整數、原子等。
字串(二進位)串接使用 <>
運算子,但字元清單作為清單,使用清單串接運算子 ++
iex> ~c"this " <> ~c"fails"
** (ArgumentError) expected binary argument in <> operator but got: ~c"this "
(elixir) lib/kernel.ex:1821: Kernel.wrap_concatenation/3
(elixir) lib/kernel.ex:1808: Kernel.extract_concatenations/2
(elixir) expanding macro: Kernel.<>/2
iex:1: (file)
iex> ~c"this " ++ ~c"works"
~c"this works"
iex> "he" ++ "llo"
** (ArgumentError) argument error
:erlang.++("he", "llo")
iex> "he" <> "llo"
"hello"
在處理完二進位、字串和字元清單之後,是時候討論鍵值資料結構了。