6

在 Unicode 中,复合字符和代理对之间有什么区别?

对我来说,它们听起来很相似——两个字符代表一个字符。这两个概念有什么区别?

4

3 回答 3

20

代理对是 Unicode 中的一个奇怪的疣。

Unicode 本身就是对数字意义的抽象分配。这就是编码。大写字母-A、希腊语-alternate-terminal-sigma、克林贡语-closure-bracket-2 等。目前,可使用最多约 2 21的数字,但并非全部都在使用。在 Unicode 的上下文中,每个数字都称为一个代码点

然而,Unicode 套件作为一个整体包含的不仅仅是这种编码。它还包含序列化 代码点的技术。这本质上只是一个序列化无符号整数的练习。指定了三个技术子族:UTF-32、UTF-8 和 UTF-16。

UTF-32 只是将每个代码点表示为 32 位无符号整数。这很容易。存在两种变体,分别用于大端和小端。每个 32 位序列化整数称为这种格式的代码单元,这是一种固定宽度的格式(每个代码单元一个代码点)。

UTF-8 是一种巧妙的多字节格式,其中代码点占用 1 到 6 个 8 位字节。这种格式非常便携,因为它没有排序问题,而且对于英语、近英语和计算机代码来说非常紧凑。UTF-8 的代码单元是一个字节,这是一种可变宽度格式(每个代码点 1-6 个代码单元)。

最后是 UTF-16:最初,人们认为 Unicode 只能处理 2个 16的数字,因此最初认为它是固定宽度的,具有 16 位代码单元。然而,最终很明显我们需要更大的数字。所以 UTF-16 现在也是一种可变宽度格式,但实现这一点的方法是某些 16 位代码单元充当指示符,表明它们是两个单元对(代理对)的一部分。但是,为了简化您检测​​这些对的方式,而不是像 UTF-8 那样使用一些外部信封格式,代理使用的实际 16 位值被故意泄漏回 Unicode 编码并被排除在编码之外- 也就是说,代理值 0xD800 到 0xDFFF不是有效的 Unicode 代码点。

因此,总而言之,代理项是强制将 Unicode 的序列化格式重新纳入编码的结果,并扭曲了编码的设计以适应序列化格式。这也许是一个不幸的历史事故,回想起来有些无意义和难看,但这是我们所拥有的,也是我们需要忍受的。


另一方面,复合字符是更高层次的东西:它们是由多个 Unicode 代码点组成的视觉单元(“字素”)。有时人们将代码点本身称为“字符”,但这有点误导,因为字符实际上应该是字素,并且它们可以由多个组件组成(例如基本字母加上变音符号和修饰符)。

于 2014-03-01T22:37:20.043 回答
6

复合字符的一个例子是 Unicode U+0039, É. 它应该与分解的对 U+0045E和 U+0301(组合的重音符号)显示相同。这与用于实际存储字符的任何字节编码无关;这只是使用 Unicode 表示相同图形字符的两种不同方式。

代理对是 UTF-16 特有的,它使用两个 16 位值来表示大于 U+FFFF 的单个 Unicode 代码点(显然不能容纳在单个 16 位值中)。例如(来自维基百科文章),代码点 U+1D11E 被序列化为两个 16 位值 0xD834 和 0xDD1E。(用于表示它们的实际字节序列将取决于您使用的是大端还是小端版本的 UTF-16。)

于 2014-03-01T22:37:17.550 回答
1

TL;博士

  • 复合字符:e¨→ ë
  • 代理对:0xD83D + 0xDCA9

长版

复合字符(与现成的)

取字符串Noël

它在 Unicode 中有两种表示形式:

  • 诺埃尔
  • 诺埃尔

你可能分不出区别。一个由四个代码单元组成,另一个由五个组成:

  • 诺埃尔Noe¨l
  • 诺埃尔Noël

其中一个使用“复合”字符,另一个使用“现成”字符:

  • e¨U+0065 Latin Small Letter E U+0308 Combining Diaeresis
  • ëU+00EB Latin Small Letter E With Diaeresis

换句话说:

  • Noël:使用“复合 ë 字符”
  • Noël:使用“现成的ë字符”

重要的是要注意这些字符串是相同的。这两个字符串代表同一个词,特别是同一个字符。除了碰巧有一个“现成的”角色。

并非每个角色都有“现成的”等价物。例如:

  • ̊q:q ˚

一个上面有环的小拉丁 q。没有现成的版本,你必须使用组合变音符号。如果有现成的版本,那只是意味着它们是同一角色的两种不同表现形式。

所以这是一个“复合角色”:它与“现成角色”相反。

代理对

让我们再看看Noël(使用现成角色的那个)。它由4个字符组成:

  • Noël
  • U+004E U+006F U+00EB U+006C

是四个数字:

UInt32[] text = [0x0000004E, 0x0000006F, 0x000000EB, 0x0000006C];

这些数字恰好都小于 16 位,所以很多人可能会倾向于使用 UInt16 数组:

UInt16[] text = [0x004E, 0x006F, 0x00EB, 0x006C];

问题是并非每个 unicode 字符都是 16 位的。Unicode 字符是完整的 32 位。

举个例子:

  • U+1F449 U+1F351 U+1F44D

为此,我们需要完整的 32 位来表示每个字符:

UInt32 text = [0x0001F449, 0x0001F351, 0x0001F44D];

这一切都很好,很好,很实用。

但人们讨厌 32 位数字

人们觉得用一个完整的 32 位来表示每个字符是一种浪费。而且既然整个世界基本上都说英语,难道没有办法我们可以主要使用 16 位来代替吗?

输入 UTF-16

人们想出了一种巧妙的方法来尝试将 32 位数字填充到 16 位数组中。

让我们看看U+1F4A9(),它是各种编码:

  • UInt32[] poop32 = [0x0001F4A9];//UTF-32
  • UInt16[] poop16 = [0xD83D, 0xDCA9];//UTF-16
  • UInt8[] poop8 = [0xF0, 0x9F, 0x92, 0xA9];//UTF-8

您会看到在 UTF-16 中,为了表示字符,您需要 2 个代码点

  • 0xD83D + 0xDCA9

这两个价值观必须结合在一起。他们是一——代理对。如果您省略了第二个 UInt16,那么您会留下一些无效的内容:

  • 0xD83D无效!
于 2022-01-01T00:38:11.770 回答