【问题标题】:UTF8, codepoints, and their representation in Erlang and ElixirUTF8、代码点及其在 Erlang 和 Elixir 中的表示
【发布时间】:2018-07-02 17:32:46
【问题描述】:

查看 Elixir 对 unicode 的处理:

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

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

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

iex(69)> ?§
167

太棒了;然后我查看 UTF-8 编码表,并将值 c2 a7 视为 char 的十六进制编码。这意味着两个字节(由 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 表似乎也同意?

【问题讨论】:

  • 看起来 Erlang 将二进制的字节打印为 latin1 编码字符,而 Elixir 将它们视为 ASCII。
  • 要了解 Erlang shell 中发生了什么,您需要知道您使用的是哪个版本的 Erlang(但任何最近的版本都应该使用 Unicode),您的语言环境设置是什么(LANG,LC_ *) 以及您的终端设置使用的编码。从您的示例看来,您使用的是非 unicode 语言环境,并且终端打印 latin-1。

标签: unicode erlang elixir


【解决方案1】:

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

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

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

在 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 的整数,它会被截断。所以最后一个例子实际上是在做&lt;&lt;49831&gt;&gt;,当它被截断后变成&lt;&lt;167&gt;&gt;,这又等同于latin1中的&lt;&lt;"§"&gt;&gt;

【讨论】:

  • 这就是不推荐使用\x{...} 的原因。将\xHH 用于单字节值,将\uHHHH 用于Unicode 代码点。 &lt;&lt;"\uc2a7"&gt;&gt; #=&gt; "슧"
  • "在 latin1 中,代码点 § (0xA7) 也由字节 0xA7 表示。" 确实,这是我懒得检查的东西,我的错!
  • 当然还有关于截断的部分真的画完了,谢谢!
【解决方案2】:

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

为了存储字符,您必须将代码点转换为一些字节序列。有几种不同的方法可以做到这一点;每一种都称为 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 编码中只占用一个字节是 Unicode 之前的默认值。

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

其余字节的高两个位都设置为 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 个代码点,情况并非如此。

【讨论】:

  • 也谢谢你(虽然我已经接受了答案)。我查看的其中一页将十六进制值(两个字节)称为“代码”,我将其解释为“代码点”,这最终让我很困惑。
  • “巧合”当然也逃过了我的视线。
猜你喜欢
  • 2011-11-30
  • 2016-07-10
  • 1970-01-01
  • 2023-03-13
  • 2023-03-27
  • 2014-03-31
  • 2017-11-21
  • 2017-08-07
  • 2015-05-15
相关资源
最近更新 更多