查看源代码 二进制、字符串和字符列表
在 "基本类型" 中,我们学习了关于字符串的一些知识,并使用 is_binary/1
函数进行检查。
iex> string = "hello"
"hello"
iex> is_binary(string)
true
在本章中,我们将深入了解二进制的本质,它们与字符串的关系,以及 Elixir 中单引号值 'like this'
的含义。虽然字符串是计算机语言中最常见的 数据类型之一,但它们在细微之处却很复杂,而且经常被误解。要理解 Elixir 中的字符串,我们必须了解 Unicode 和字符编码,特别是 UTF-8 编码。
Unicode 和码位
为了促进跨越多种语言的计算机之间进行有意义的通信,需要一个标准,以便一台机器上的 0 和 1 在传输到另一台机器时具有相同的含义。 Unicode 标准 充当几乎所有已知字符的官方注册表:这包括来自古典和历史文本的字符、表情符号,以及格式化和控制字符。
Unicode 将其所有字符都组织到码位表中,每个字符都有一个唯一的数值索引。这个数值索引被称为 码位。
在 Elixir 中,可以在字符字面量前面使用 ?
来揭示其码位。
iex> ?a
97
iex> ?ł
322
请注意,大多数 Unicode 码位表会使用其十六进制 (hex) 表示来引用码位,例如 97
在十六进制中转换为 0061
,我们可以在 Elixir 字符串中使用 \uXXXX
符号和其码位号的十六进制表示来表示任何 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 足够聪明,知道它们被视为单个字符,因此长度仍然为 1。
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
在用 4 个比特表示时,其二进制形式为 0011
,相当于 0
、0
、1
、1
这四个值,每个值使用 1 个比特存储。
iex> <<0::1, 0::1, 1::1, 1::1>> == <<3::4>>
true
任何超出分配的比特数所能存储的值都将被截断。
iex> <<1>> == <<257>>
true
这里,257 的二进制表示为 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
符号(我们将在 "符号" 章节中介绍符号)表示我们正在处理字符列表,而不是常规字符串。
字符列表不包含字节,而是包含整数码位。但是,只有当所有码位都在 ASCII 范围内时,列表才会以符号的形式打印出来。
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"
在介绍了二进制、字符串和字符列表之后,是时候谈谈键值数据结构了。