UTF8,代码点,以及它们在Erlang和Elixir中的表示



通过Elixir对Unicode的处理:

iex> String.codepoints("abc§")
["a", "b", "c", "§"]

非常好,其中 byte_size/2 不是 4 而是 5,因为最后一个字符占用 2 个字节,我明白了。

?运算符(或者它是一个宏?找不到答案)告诉我

iex(69)> ?§
167

太好了;然后我查看 UTF-8 编码表,并将值c2 a7视为字符的十六进制编码。这意味着两个字节(如 byte_size/1 所示)是 c2(十进制为 94)和 a7(十进制为 167)。那个 167 是我之前评估时得到的结果?§。我不明白的是,确切地说..为什么该数字是"代码点",根据运算符的说明。当我尝试逆向工作并评估二进制文件时,我得到了我想要的:

iex(72)> <<0xc2, 0xa7>>
"§"

为了让我完全吃香蕉,这就是我在 Erlang shell 中得到的:

24> <<167>>.
<<"§">>
25> <<"x{a7}">>.
<<"§">>
26> <<"x{c2}x{a7}">>.
<<"§"/utf8>>
27> <<"x{c2a7}">>.    
<<"§">>

!! 而 Elixir 只对上面的代码感到满意......我有什么不明白的?为什么 Erlang 对单字节非常满意,因为 Elixir 坚持 char 需要 2 个字节 - 而 Unicode 表似乎同意?

码位是分配给字符的数字。它是一个抽象值,不依赖于实际内存中某处的任何特定表示。

为了存储字符,您必须将代码点转换为某个字节序列。有几种不同的方法可以做到这一点;每个都称为 Unicode 转换格式,并命名为 UTF-n,其中n是编码基本单位中的位数。曾经有一个 UTF-7,用于假设 7 位 ASCII,甚至无法可靠地传输字节的第 8 位;在现代系统中,有 UTF-8、UTF-16 和 UTF-32。

由于最大的码位值适合 21 位,UTF-32 是最简单的;您只需将码位存储为 32 位整数。(理论上可能存在 UTF-24 甚至 UTF-21,但常见的现代计算平台自然会处理恰好占用 8 位或 16 位倍的值,并且必须更加努力地处理其他任何内容。

所以 UTF-32 很简单,但效率低下。它不仅有 11 个永远不需要的额外位,还有 5 个几乎不需要的位。在野外发现的大多数Unicode字符都在基本多语言平面中,U + 0000到U + FFFF。UTF-16 允许您将所有这些代码点表示为纯整数,占用 UTF-32 一半的空间。但是它不能以这种方式表示从 U+10000 开始的任何内容,因此 0000-FFFF 范围的一部分被保留为"代理对",可以组合在一起表示具有两个 16 位单元的高平面 Unicode 字符,总共 32 位,但仅在需要时。

Java在内部使用UTF-16,但Erlang(以及Elixir)以及大多数其他编程系统使用UTF-8。UTF-8 的优点是与 ASCII 完全透明兼容 - ASCII 范围内的所有字符(U+0000 到 U+007F 或 0-127 十进制)都由具有相应值的单个字节表示。但是,任何码位超出 ASCII 范围的字符都需要一个以上的字节 - 即使是 U+0080 到 U+00FF、十进制 128 到 255 范围内的字符,这些字符在 Latin-1 编码中只占用一个字节,而 Latin-1 编码曾经是 Unicode 之前的默认值。

因此,对于Elixir/Erlang"二进制文件",除非你特意以不同的方式编码,否则你使用的是UTF-8。如果您查看 UTF-8 字符的第一个字节的高位,它要么是 0,表示您有一个单字节 ASCII 字符,要么是 1。如果它是 1,那么第二高的位也是 1,因为在你到达 0 位之前从高位倒计时的连续 1 位数会告诉您该字符总共占用了多少字节。 因此,模式 110xxxxx 表示字符是两个字节,1110xxxx 表示三个字节,11110xxx 表示四个字节。(没有合法的 UTF-8 字符需要超过 4 个字节,尽管该编码理论上最多可以支持七个字节。

其余字节的两个高位都设置为 10,因此它们不会被误认为是字符的开头。其余位是代码点本身。

以您的案例为例,"§"的代码点是 U+00A7 - 即十六进制 A7,即十进制 167 或二进制10100111。由于它大于十进制 127,因此需要两个 UTF-8 字节。这两个字节将具有二进制形式110abcde 10fghijk,其中位abcdefghijk将保存代码点。因此,代码点的二进制表示形式10100111被填充为00010100111并拆分为序列 00010(替换 UTF-8 模板中的abcde)和 100111(替换fghijk)。这将产生两个二进制值为 11000010 和 10100111 的字节,它们是十六进制的 C2 和 A7,或十进制的 194 和 167。

您会注意到第二个字节巧合地与您正在编码的代码点具有相同的值,但重要的是要意识到这种对应关系只是一个巧合总共有 64 个码位,从 128 (U+0080) 到 191 (U+00BF),都是这样工作的:它们的 UTF-8 编码由十进制值为 194 的字节组成,后跟一个值等于码位本身的字节。但是对于 Unicode 中可能的其他 1,114,048 个代码点,情况并非如此。

代码点是标识 Unicode 字符的内容。§ 的代码点是 167 (0xA7)。代码点可以以不同的方式以字节表示,具体取决于您选择的编码。

这里的混淆来自这样一个事实,即代码点 167 (0xA7) 由编码为 UTF-8 时0xC2 0xA7的字节标识。

当您将 Erlang 添加到对话中时,您必须记住 Erlang 默认编码是/是 latin1(正在努力迁移到 UTF-8,但我不确定它是否迁移到 shell - 有人请纠正我)。

在 latin1 中,代码点 § (0xA7) 也由字节0xA7表示。因此,直接解释您的结果:

24> <<167>>.
<<"§">> %% this is encoded in latin1
25> <<"x{a7}">>.
<<"§">> %% still latin1
26> <<"x{c2}x{a7}">>.
<<"§"/utf8>> %% this is encoded in utf8, as the /utf8 modifier says
27> <<"x{c2a7}">>.
<<"§">>  %% this is latin1

最后一个非常有趣,可能令人困惑。在 Erlang 二进制文件中,如果传递的值大于 255 的整数,则会截断该整数。所以最后一个例子是有效地做<<49831>>当被截断时变成<<167>>,这又等同于拉丁语1中的<<"§">>

最新更新