总结一下问题的答案
2 字节 char 数据类型不足以处理 Unicode 字符串中的“字符”概念吗?
is 是的,它不足以存储 Unicode 字符,但您无需担心,因为您不使用也不应该使用它来迭代
See also
- 16 位 wchar_t 是否正式有效用于表示完整的 Unicode? https://stackoverflow.com/q/39548465/995714
- C++ wchar_t 和 wstrings 有什么“问题”?宽字符有哪些替代方案? https://stackoverflow.com/q/11107608/995714
欲了解更多详情,请阅读下文
在处理字符串时,您不应该始终使用对代码点进行操作的方法吗?
人们几乎不应该这样做,因为与普遍的看法相反,UTF-32 中的字符not有固定长度。 UTF-32 只是一种固定长度编码单个代码点, but a 用户感知的角色可以由多个代码点表示:
重要的是要认识到,用户所认为的“字符”(语言书写系统的基本单位)可能不仅仅是单个 Unicode 代码点。相反,该基本单元可能由多个 Unicode 代码点组成。为了避免计算机使用术语“字符”产生歧义,这称为“用户感知的字符”。例如,“G”+ 重音符号是用户感知的字符:用户将其视为单个字符,但实际上由两个 Unicode 代码点表示。这些用户感知的字符通过所谓的字素簇来近似,可以通过编程方式确定。
字素簇边界对于排序规则、正则表达式、UI 交互、垂直文本分割、首字母样式边界识别以及文本中“字符”位置的计数非常重要。
Unicode 文本分割 - 字素簇边界 https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
所以我们应该只使用字素 A.K.A用户感知的角色相反,并且不得将段分解为代码点。例如,人们通常会迭代字符串来查找特定字符,如果我们想找到稻穗????U+1F33E那么它会意外地匹配????????,因为农民表情符号被编码为U+1F468 U+200D U+1F33E。然后该索引可用于将子字符串从 ???? 转移到其他内容,这可能会让用户大吃一惊。看为什么像 ???????????????? 这样的表情符号在 Swift 字符串中的处理方式如此奇怪? https://stackoverflow.com/q/43618487/995714
另一个常见的错误是人们将字符串截断到第一个或最后一个N
字符和附加/前置"..."
当它太长时将其放入 UI,然后它会严重崩溃,因为char
在索引处N
可能位于字素簇的中间。例如"????????????????????????️????????❤️????????????"
是一个不太长的字符串3个用户感知的角色但它是由21个码位因此,如果您在第 20 个字符处截断,那么它会完全弄乱输出字符串。或者检查印度语字符串"ফোল্ডার"
可以很容易地看出有4 个字符通过使用鼠标或箭头键选择或迭代它(尽管我必须承认我不是任何印度语言的专家),但它被编码为7 个代码点 (U+09AB U+09CB U+09B2 U+09CD U+09A1 U+09BE U+09B0)并且在中间被截断时会表现得很糟糕。如果不考虑多代码点字符,反转字符串或查找回文将具有相同的命运
印度语和阿拉伯语(卡纳达语、孟加拉语、泰语、缅甸语、老挝语、马来语、印地语、波斯语、阿拉伯语、泰米尔语...)大量使用ZWJ https://en.wikipedia.org/wiki/Zero-width_joiner and ZWNJ https://en.wikipedia.org/wiki/Zero-width_non-joiner修改字符。在这些语言中,当没有 ZWJ 时,字符也会相互组合或相互修改,如前面的示例字符串所示。其他一些例子:நி (U+0BA8 U+0BBF), षि (U+0937 U+093F)。如果删除中间的代码点或获取子字符串,则它可能无法按预期工作。许多语言,如缅甸语、蒙古语、CJKV...以及数学符号和表情符号也使用变化 https://en.wikipedia.org/wiki/Variation_Selectors_%28Unicode_block%29 选择器 https://en.wikipedia.org/wiki/Variation_Selectors_Supplement (VS https://stackoverflow.com/q/4974668/995714) 调整前一个字符。例如က︀ (U+1002 U+FE00)、ဂ︀ (U+1000 U+FE00)、င︀ (U+1004 U+FE00)、⋚︀ (U+22DA U+FE00)、丸︀ (U +4E38 U+FE00)。这是替代变体的完整列表 https://unicode.org/Public/UCD/latest/ucd/StandardizedVariants.txt。删除 VS 将更改文档的呈现,这可能会影响含义或可读性。您不能在国际化应用程序中任意轻松地获取子字符串
您可以查看书写系统和 Unicode 简介 - 复杂脚本渲染 https://r12a.github.io/scripts/tutorial/part3 and 复杂的文本布局 https://en.wikipedia.org/wiki/Complex_text_layout如果您对有关这些脚本的更多信息感兴趣
有些人提到了使用组合字符 https://en.wikipedia.org/wiki/Combining_character像 g̈ (U+0067 U+0308), Å (U+0041 U+030A) 或 é (U+0065 U+0301),但这只是一个tiny不常见的用例,其中一个字符由多个代码点表示,并且通常可转换为预制字符 https://en.wikipedia.org/wiki/Precomposed_character。在许多其他语言中,这种组合序列更为常见,并且不利于文本的呈现。我将举一些例子[]
以及中规定的一些规则Unicode 文本分割 https://unicode.org/reports/tr29/:
-
Do not break Hangul syllable sequences.
- [ 在韩语中,字符可以由以下组成:Jamos https://en.wikipedia.org/wiki/List_of_Hangul_jamo: 훯 (U+D6E0 U+11B6), 가 (U+1100 U+1161), 각 (U+1100 U+1161 U+11A8), 까ᇫ (U+1101 U+1161 U+11EB)。除了一些奇怪的标准 https://devblogs.microsoft.com/oldnewthing/20201009-00/?p=104351 ]
-
Do not break before extending characters or ZWJ.
- [ 例如印度字符,如 ൺ (U+0D23 U+0D4D U+200D), ല് (U+0D32 U+0D4D U+200D), ര് (U+0D30 U+0D4D U+200D), क् (U+0915 U+094D U+200D) ]
-
Do not break within emoji modifier sequences or emoji zwj sequences.
- [ ????????♀️ (U+1F3C3 U+1F3FB U+200D U+2640 U+FE0F), ????????♀️ (U+1F3C3 U+1F3FF U+200D U+2640 U+FE0F), ???????????????? (U+1F469 U+200D U+1F469 U+200D U+1F466 U+200D U+1F466), ???????????????? (U+1F468 U+200D U+1F469 U+200D U+1F466 U+200D U+1F466), ????????️ (这是一个超宽表情符号,而不是两个,由 U+1F636 U+200D U+1F32B U+FE0F 组合而成), ????❤️???? (U+1F468 U+200D U+2764 U+FE0F U+200D U+1F468), ????????❤️???????? (U+1F469 U+1F3FC U+200D U+2764 U+FE0F U+200D U+1F468 U+1F3FD), ????????❤️???????????? (U+1F469 U+1F3FB U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F469 U+1F3FF), ???????? (U+1F431 U+200D U+1F680), ???????? (U+1F431 U+200D U+1F464), ???????? (U+1F431 U+200D U+1F409), ???????? (U+1F431 U+200D U+1F4BB), ???????? (U+1F431 U+200D U+1F453), ???????? (U+1F431 U+200D U+1F3CD), ???????? (U+1F467 U+1F3FB), ???????? (U+1F935 U+1F3FB), ❤️ (U+2764 U+FE0F), 1️⃣ (U+0031 U+FE0F U+20E3), ⚕️ (U+2695 U+FE0F), ©️ (U+00A9 U+FE0F), ®️ (U+00AE U+FE0F), ‼️ (U+203C U+FE0F), ™️ (U+2122 U+FE0F), ☑︎ (U+2611 U+FE0E), ????☠ (U+1F3F4 U+200D U+2620 U+FE0F), ????️⚧ (U+1F3F3 U+FE0F U+200D U+26A7 U+FE0F), ????️???? (U+1F3F3 U+FE0F U+200D U+1F308)]。注意:上述某些表情符号可能无法在您的系统上正确显示,因为它们是特定于平台的
-
Do not break within emoji flag sequences. That is, do not break between regional indicator (RI) symbols if there is an odd number of RI characters before the break point.
- [ 州/地区标志通常由 2 个地区指示符号创建,例如 ???????? (U+1F1FB U+1F1F3), ???????? (U+1F1FA U+1F1F8), ???????? (U+1F1EC U+1F1E7), ???????? (U+1F1EF U+1F1F5), ???????? (U+1F1E9 U+1F1EA), ???????? (U+1F1EB U+1F1F7), ???????? (U+1F1EA U+1F1FA), ???????? (U+1F1FA U+1F1F3)]。注意:您可能只会看到这些字母,尤其是在 Windows 上,因为微软以某种方式拒绝将标志表情符号添加到他们的平台上
字素簇边界规则 https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules
恐怕有人必须向我解释为什么世界仍在使用 char,而这似乎已经过时了。
正如所说,没有人应该迭代char
s 在字符串中,无论是否char
长度为 1、2 或 4 字节。这最正确的方法是迭代字素。像 ICU 这样的优秀 Unicode 库会对您有所帮助。上面的Unicode文档中也提到了这一点
就用户而言,文本的底层表示并不重要,但重要的是编辑界面呈现用户所认为的字符的统一实现。默认情况下,对于诸如首字下沉格式设置以及文本选择、箭头键移动或文本退格等操作的实现,字素簇可以被视为单位。例如,当字素簇在内部由由基本字符 + 重音符号组成的字符序列表示时,使用右箭头键将从基本字符的开头跳到最后一个重音符号的末尾。
Unicode 文本分割 - 字素簇边界 https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
不幸的是,在许多情况下,由于缺乏适当的 Unicode 库,这是不可能的,所以在这种情况下,人们可以迭代代码点相反,但他们需要小心避免匹配或剪切字素中间的字符串,从而破坏用户感知的字符
事实上,许多现代语言通过使用通常称为的类型来阻止您迭代字符串中的字节"rune" https://stackoverflow.com/q/19310700/995714相反,它实际上是 UTF-32,并且避免经典char
完全地或者只是将其作为遗留类型。例如,在 Go 中我们有rune https://go.dev/blog/strings在 C# 中有System.Text.Rune https://learn.microsoft.com/en-us/dotnet/api/system.text.rune?view=net-5.0。在生锈时strings https://doc.rust-lang.org/book/ch08-02-strings.html are 以 UTF-8 格式存储 https://doc.rust-lang.org/std/string/struct.String.html but 迭代 https://doc.rust-lang.org/book/ch08-02-strings.html#methods-for-iterating-over-strings完成于char https://doc.rust-lang.org/std/primitive.char.html(它代表 Unicode 标量值而不是byte):
for b in "नमस्ते".bytes() {
println!("{}", b);
}
python3 中的循环以类似的方式完成:
>>> s = "abc\u20ac\U00010302\U0010fffd"
>>> len(s)
6
>>> for c in s:
... print('U+{:04X}'.format(ord(c)))
...
U+0061
U+0062
U+0063
U+20AC
U+10302
U+10FFFD
可以看出,你永远不会循环遍历每个byte在他们中。字符串迭代是在符文上完成的,因此底层字符串编码完全不相关。实现可以使用 UTF-8、UTF-16、UTF-32 或任何 Unicode 编码,但用户仍然对此一无所知,因为他们只与符文交互。