6-二进制串、字符串和字符列表

在“基本类型”一章中,介绍了字符串,以及使用is_binary/1函数检查它:

  1. iex> string = "hello"
  2. "hello"
  3. iex> is_binary string
  4. true

本章将学习理解:二进制串(binaries)是个啥,它怎么和字符串(strings)扯上关系的; 以及用单引号包裹的值'like this'是啥意思。

UTF-8和Unicode

字符串是UTF-8编码的二进制串。 为了弄清这句话的准确含义,我们要先理解两个概念:字节(bytes)和字符编码(code point)的区别。 Unicode标准为我们已知的大部分字母分配了字符编码。 比如,字母a的字符编码是97,而字母ł的字符编码是322。 当把字符串"hełło"写到硬盘上的时候,需要将字符编码转化为字节。 如果我们遵循一个字节表示一个字符编码这个,那是写不了"hełło"的。 因为字母ł的编码是322,而一个字节所能存储的数值范围是0255。 但是如你所见,确实能够在屏幕上显示"hełło",说明还是有某种解决方法的,于是编码便出现了。

要用字节表示字符编码,我们需要用某种方式对其进行编码。 Elixir选择UTF-8为主要并且默认的编码方式。 当我们说某个字符串是UTF-8编码的二进制串,指的是该字符串是一串字节, 这些字节以某种方式(即UTF-8编码)组织起来,表示特定的字符编码。

因为给字母ł分配的字符编码是322,因此在实际上需要一个以上的字节来表示。 这就是为什么我们会看到,调用函数byte_size/1String.length/1的结果不一样:

  1. iex> string = "hełło"
  2. "hełło"
  3. iex> byte_size string
  4. 7
  5. iex> String.length string
  6. 5

注意:如果你使用Windows,你的终端有可能不是默认使用UTF-8编码方式。你需要在进入iex(iex.bat)之前, 首先执行chcp 65001命令来修改当前Session的编码方式。

UTF-8需要1个字节来表示heo的字符编码,用2个字节表示ł。 在Elixir中可以使用?运算符获取字符的编码:

  1. iex> ?a
  2. 97
  3. iex>
  4. 322

你还可以使用 String模块里的函数 将字符串切成单独的字符编码:

  1. iex> String.codepoints("hełło")
  2. ["h", "e", "ł", "ł", "o"]

Elixir为字符串操作提供了强大的支持,它支持Unicode的许多操作。实际上,Elixir通过了文章 “字符串类型崩坏了” 记录的所有测试。

然而,字符串只是故事的一小部分。如果字符串正如所言是二进制串,那我们使用is_binaries/1函数时, Elixir必须一个底层类型来支持字符串。事实亦如此,下面就来介绍这个底层类型—-二进制串。

二进制串(以及比特串bitstring

在Elixir中可以用<<>>定义一个二进制串:

  1. iex> <<0, 1, 2, 3>>
  2. <<0, 1, 2, 3>>
  3. iex> byte_size(<<0, 1, 2, 3>>)
  4. 4

一个二进制串只是一连串的字节而已。这些字节可以以任何方式组织,即使凑不成一个合法的字符串:

  1. iex> String.valid?(<<239, 191, 191>>)
  2. false

而字符串的拼接操作实际上就是二进制串的拼接操作:

  1. iex> <<0, 1>> <> <<2, 3>>
  2. <<0, 1, 2, 3>>

一个常见技巧是,通过给一个字符串尾部拼接一个空(null)字节<<0>>, 可以看到该字符串内部二进制串的样子:

  1. iex> "hełło" <> <<0>>
  2. <<104, 101, 197, 130, 197, 130, 111, 0>>

二进制串中的每个数值都表示一个字节,其数值最大范围是255。 二进制允许使用修改器显式标注一下那个数值的存储空间大小,使其可以存储超过255的数值; 或者将一个字符编码转换为utf8编码后的形式(变成多个字节的二进制串):

  1. iex> <<255>>
  2. <<255>>
  3. iex> <<256>> # 被截断(truncated)
  4. <<0>>
  5. iex> <<256 :: size(16)>> # 使用16比特(bits)即2个字节来保存
  6. <<1, 0>>
  7. iex> <<256 :: utf8>> # 这个数字是一个字符的编码,将其使用utf8方式编码为字节
  8. "Ā" # 注意,在iex交互窗口中,所有可以作为合法字符串的二进制串,都会显示为字符串
  9. iex> <<256 :: utf8, 0>> # 尾部拼接个空字节,查看上一条命令结果内部实际的二进制串
  10. <<196, 128, 0>>

如果一个字节是8个比特,那如果我们给一个大小是1比特的修改器会怎样?:

  1. iex> <<1 :: size(1)>>
  2. <<1::size(1)>>
  3. iex> <<2 :: size(1)>> # 被截断(truncated)
  4. <<0::size(1)>>
  5. iex> is_binary(<< 1 :: size(1)>>) # 二进制串失格
  6. false
  7. iex> is_bitstring(<< 1 :: size(1)>>)
  8. true
  9. iex> bit_size(<< 1 :: size(1)>>)
  10. 1

这样(每个元素长度是1比特)就不再是二进制串(人家每个元素是一个字节,起码8比特), 退化成为比特串(bitstring),意思就是一串比特! 所以,所以,二进制串就是一特殊的比特串,比特总数是8的倍数。

也可以对二进制串或比特串做模式匹配:

  1. iex> <<0, 1, x>> = <<0, 1, 2>>
  2. <<0, 1, 2>>
  3. iex> x
  4. 2
  5. iex> <<0, 1, x>> = <<0, 1, 2, 3>>
  6. ** (MatchError) no match of right hand side value: <<0, 1, 2, 3>>

注意,在没有修改器标识的情况下,二进制串中的每个元素都应该匹配8个比特长度。 因此上面最后的例子,匹配的左右两端不具有相同容量,因此出现错误。

下面是使用了修改器标识的匹配例子:

  1. iex> <<0, 1, x :: binary>> = <<0, 1, 2, 3>>
  2. <<0, 1, 2, 3>>
  3. iex> x
  4. <<2, 3>>

上面例子使用了binary修改器,指示x是个二进制串。(为啥不用单词的复数形式binaries搞不懂啊。) 这个修改器仅仅可以用在被匹配的串的末尾元素上。

跟上面例子同样的原理,使用字符串的连接操作符<>,效果相似:

  1. iex> "he" <> rest = "hello"
  2. "hello"
  3. iex> rest
  4. "llo"

关于二进制串/比特串的构造器<< >>完整的参考, 请见Elixir的文档

总之,记住字符串是UTF-8编码后的二进制串,而二进制串是特殊的、元素数量是8的倍数的比特串。 尽管这种机制增加了Elixir在处理比特或字节时的灵活性, 而现实中99%的时候你只会用到is_binary/1byte_size/1函数跟二进制串打交道。

字符列表(char lists)

字符列表就是字符的列表。

  1. iex> 'hełło'
  2. [104, 101, 322, 322, 111]
  3. iex> is_list 'hełło'
  4. true
  5. iex> 'hello'
  6. 'hello'

可以看出,比起包含字节,一个字符列表包含的是单引号所引用的一串字符各自的字符编码。 注意IEx遇到超出ASCII值范围的字符编码时,显示其字符编码的值,而不是字符。 双引号引用的是字符串(即二进制串),单引号表示的是字符列表(即,一个列表)。

实际应用中,字符列表常被用来做为同一些Erlang库交互的参数,因为这些老库不接受二进制串作为参数。 要将字符列表和字符串之间相互转换,可以使用函数to_string/1to_char_list/1

  1. iex> to_char_list "hełło"
  2. [104, 101, 322, 322, 111]
  3. iex> to_string 'hełło'
  4. "hełło"
  5. iex> to_string :hello
  6. "hello"
  7. iex> to_string 1
  8. "1"

注意这些函数是多态的。它们不但可以将字符列表转化为字符串,还能转化整数、原子等为字符串。