Skip to content

Latest commit

 

History

History
300 lines (170 loc) · 38.8 KB

criticisms-on-UTF-8-everywhere-manifesto.md

File metadata and controls

300 lines (170 loc) · 38.8 KB

《UTF-8 遍地开花宣言》批判

Created @ 2016-10-17 01:30, rev v5 @ 2016-10-17 01:54.

概述

以下评论基于中译版本

一些科普常识和结论(例如 UTF-16 因为变长特性导致的无能,以及代码页是不合理的烂设计)是没有问题的。但是,从动机、论证的目的以及参照的依据看,存在相当多的低级错误,应予视为误导。

1-3

第一个错误是混淆了“文本”的用途。文本首先是人类书写系统的直观具象。文本编码的最初设计也遵循这个基本要点,虽然之后作为妥协混入了“控制字符”也已有相当长的历史了,但那只应该是妥协。

文中提到文本作为通信协议表现形式的常用现状。这仍然只是妥协。注意通常称为二进制编码的非文本是无法被文本取代的,因为严格地来说文本充其量只是二进制编码的一种。原用于反映书写系统、之后作为妥协折衷产物的这种形式具有非常多不适合编码的普遍糟糕特性(例如浪费资源),只是体现不合理的 worse is better 渣设计的历史包袱的实例而已,其被普遍接受并非来自于自身的技术优势——换句话说,也是妥协。

造成用户不得不“享受”这种现状的,早年罪魁祸首之一是 UNIX 系为首的业界通行设计,近年来则是 Web 。Unicode 承前启后不假,但在“流行”充其量是拾人牙慧—— UTF-8 几乎全是借 Web 的东风而大势所趋,尽管技术上来讲对 Linux userland 来说它更能发挥作用。

这些规范在标准化的意义上来讲相当不得体。作为其中影响最巨大的几个余孽(和妥协失败品),基于 UNIX 早年对文本处理的过于简化的错误假设,ISO C 长期以来没能说清楚什么叫“字符”(character) ; ISO C++ 也有类似的问题。(并且相关术语定义的混乱和微妙的不一致仍然存在于正式(normative) 文本中。)

4

不透明“参数”是此文较有建设性的论点,可惜并没有给出实用的做法。因为语言设计中普遍缺乏约束,这基本只能靠用户(程序员)自觉,效果没法指望。更正确的姿势是,对“字符集”和编码方案透明,减少字符集特性的冗余假设。这样,简单起见只有 ASCII 自然扩展才是 UTF-8 的核心优势。“完整” Unicode 支持从头到尾都是没有说服力的。

5

之所以 Unicode 有必要单独抽出来评价,是因为在概念混淆和把妥协强行扶正为正式惯用技俩推广的不可替代的消极作用。

技术上 Unicode 有显著的问题。作为妥协的以“字符集”为中心特性之一的规范,一个明显的笑话是“字符集”无法精确定义——因为它根本就没有(也没法)规定,什么“感官”“字符”应该是字符集的收录范围。

因为不是随便谁都能往 Unicode 里添加公认接受的字符,这导致了 Unicode 扩展不同于寻常的字符集定义的流程,缺乏文化和厂商中立性,并且可能会对实现添加麻烦。

近期较为显著的例子是 emoji 。一个笑话:为什么 FreeType 突然需要依赖 libpng 才能看起来使 Unicode 渲染的支持“完整”一些呢?

6

把传输编码和文本表示编码比较是一种常识性错误。应该逆转过来,从原始需求出发:如果一开始就做正确的事——就让文本编码尽量只表示人能读写的真正意义上的纯文本,那么这样的比较还有何意义?

反过来,正是因为这种不切实际(需求)的浮夸比较,进一步助长了对不适合作为通信协议的“文本”这种形式的滥用,这是一种恶性循环。

至于只用于真正“文本”的情形,作为东亚语言母语用户,我很清楚几乎从不需要英文字母以外的那些在 UTF-8 中占用两个八元组的西欧文字,作者对此非常地想当然。另外,真要论东亚语言占用空间,而不担心随机定位性质的话,GB18030 跳出来就能把 UTF-8 打得满地找牙(嘛,本是同根生……)。

7

真正优雅的操作是把编解码和对码位的操作分离——虽然会让人怀疑为什么究竟还需要编码这回破事了。

也就是说,这方面 UTF-8 永远被 UCS-4 吊打。

8

这里没有否认对感官字符的计数不重要,却有意无意地忽略了一点,在计数能可靠到显得重要的时候,码位和感官字符是 1:1 的。主要显著例子是没有复杂拼接规则的字母文字(如英文)和东亚语言文字。

绝大多数人口——不管是不是 Unicode 的用户——只关心使用这类文字。

还有一个自以为是的错觉是了解 Unicode Normalization Forms 对所有用户都很重要。文本的(最终)用户通常不应该需要理会编码细节——这点跟“程序员不需要成为一个语言文字专家就能够正确地写一个文件复制程序了”对比,莫名喜感。

9

UTF-16 当然很烂。

性能几乎不是问题

偷换概念。空间占用难道不是性能问题?别忘了为什么 UTF-32 没有把 UTF-8 干趴。

很多人认为不应该用文本通信协议,但实际上通信协议几乎都用英语和 ASCII 字符组成,UTF-8 更有优势。对不同类的字符串用不统一的编码使复杂度大大上升,更容易引发问题。

定义不清的“统一”的编码只会掩盖问题。“几乎都用英语和 ASCII 字符组成”也仍然打不死 GB18030 ,只能说是历史包袱原因。

我们尤其认为给 C++ 标准增加 wchar_t 是一个错误,给 C++11 增加 Unicode 亦然。我们更想要一个能存储任意 Unicode 数据的基本执行字符集。

这是技术错误。让 ISO C++ 引用 ISO 10646 并不十分困难,但要求基本执行字符集的想法不切实际——即便排除 Unicode 可变版本兼容的困扰,要求运行时分辨被编码的字符需要额外的开销并不能被任意实现接受——特别是处理“文本”毫无用处的应用场景上。而只是要求容纳码位,并不需要更改执行字符集。

实际上 ISO C++ 连 ASCII 都仍然懒得完整要求。说到这里是不是又得想想 UTF-EBCDIC 哭晕在厕所呢?

标准库的 facet 有一堆设计缺陷。

依赖区域的特性本来就是一坨笑话。

10

仍然通篇逃避区分内部和外部编码这类关于选型的文本程序开发人员的基本义务,反而把问题搞得更复杂,同时强迫最终用户承担本可能不必要的额外开销。

11

但既然存在,那么说明文本不一定是给人类看的。

更不说明应该存在。

操作子字符串的方法会兴高采烈地把非 BMP 字符一刀两断,返回一个无效的字符串。

为什么判断“字符串”“有效”必须以 Unicode 为准则?

底层框架选错了内部字符串表示的方法,影响了 ASP.NET 这样的 Web 框架:网络应用几乎全要用 UTF-8 来输出(和输入)字符,在大流量的网络应用和服务中,字符串转换导致了显著开销。底层框架选错了内部字符串表示的方法,影响了 ASP.NET 这样的 Web 框架:网络应用几乎全要用 UTF-8 来输出(和输入)字符,在大流量的网络应用和服务中,字符串转换导致了显著开销。

偷换概念。某些特定的应用层不等于“网络”。

更应该说的是选错了服务的领域。要求统一编码,只剩“外部”的场景,就不应该做适配的无用功。所谓物以类聚和 garbage in garbage out 对这里的场景更合适。

不论原来 UTF-8 是否在创造时是作为一个兼容性措施,现在它比任何其它 Unicode 编码更好,也更流行。

“更好”再次被 GB18030 打脸。顺便,到现在 UTF-8 也不是任何一个官方使用东亚语言的政府强制要求支持的标准。

你不打算让你的软件设计完整支持 Unicode,是在开玩笑吗?

指望多数用户的大脑能“支持”非 BMP 字符才更加可笑——事实上是 BMP 字符也太多了。

另外同上,因为外延的不确定性,Unicode “字符集”论及“完整支持”,也是彻头彻尾的笑话。敢明确版本嘛?

那么,既然你打算支持,而非 BMP 字符罕见就不支持,除了给软件测试增加难度,实在没什么好处。

支持才是明显增加测试工作量。

然而,真正值得在意的是,真实的应用程序不怎么操作字符串——只是原封不动地传递字符串。这意味着“几乎定长”几乎没有性能优势(参见“性能”),让字符串短一点倒有可能挺重要。

莫名其妙的逻辑。码位计数可用且码位和感官字符是 1:1 时,分隔完整字符边界的操作是 O(1) 还是 O(n) 在长字符串上性能差异可能相当明显。当然这看来的确不很常见,但这恐怕也是 UTF-8 在编码东亚文字文本上唯一打得过 GB18030 的一个技术理由。

我们坚信,只要让软件 API 默认使用统一的主流编码,程序员不需要成为一个语言文字专家就能够正确地写一个文件复制程序了。

回顾上面的 normalization 打脸。

但是如果你将来打算加个配置文件或日志文件,请考虑把字符串都转换成窄字符串。这样可以免除后患。

这是常见的鸵鸟方法论。正确的做法是之前隐晦提到过的,区分“内部”和“外部”编码方案——如果不能自己发明设计,至少也需要了解应用。这才是处理文本和其它数据的程序员需要做且必须擅长做的基本功课。

我们无法接受,即使是在像文件连接这样简单的情形下,所有现有的代码都得注意到 BOM 的问题。我们无法接受,即使是在像文件连接这样简单的情形下,所有现有的代码都得注意到 BOM 的问题。

这则是彻头彻尾的愚蠢

注意 UTF-8 的 T 的含义。所谓字节序标记本质上是传输时必要的元数据。只有附加假定以字节流传输,才可以省略字节序时指望正确的数据流——但是不一定包括边界。正确的传输总是需要这种附加的 cookie ,区别无非是哪一层做罢了。提供字节序是对边界的一种附加的(不完全)强制验证。在必须考虑边界时,丢掉附加验证并不会更“简单”。

另外,当处理“文件”这种作为外部传输的单独映像时,二进制文件的所谓文件头的元数据基本是不会被任意丢弃,处理“文本”附加额外的不一致的抽象,对“简单”而言,得不偿失。

只有假定没有边界(如 UNIX 的 cat 传输的数据)时才需要显式丢弃元数据的干扰。但这种缺乏类型标记的非结构化数据传输即便是对 shell 编程这种用途来讲,都是不怎么靠谱的,基本上只适合用完即弃。

另外,Unicode 标准实际上并没有显式鼓励传输时丢弃字节序的做法——在数据是已知 UTF-8 有效载荷的情况下,加入冗余 BOM 当然是添乱,所以应予避免。

永远使用 \n (0x0a) 作为行尾标记,即使是在 Windows 上。文件应以二进制模式读写,这保证了互操作性——一个程序在任何系统上都会给出相同结果。

依赖本不必要涉及的二进制表示通常是灾难的开始。

既然 C 和 C++ 标准采用了 \n 作为内部行尾表示,这就导致了所有文件会以 POSIX 惯例输出。

纯属谣言。

文件在 Windows 上用“记事本”打开可能会出问题;然而任何像样的文本编辑器都能理解这样的行尾。

而且,“实际”并不能保证行尾可信。记事本就是一个假定行尾二进制表示的反面例子。

至于为什么这么麻烦嘛……怪一开始的“妥协”好了。不过,考虑控制字符的设计,CR+LF 在含义上是比 LF 要合理的。

另外,实际上很多(应用层网络)协议就是规定用 CR+LF 而不是 LF ,为何在此反复?

我们也偏好 SI 单位、ISO-8601 日期格式,用句点而不是逗号作为小数点。

SI 近年来还改了基本单位定义ISO 8601 一坨几种来着……

一个更稠密的编码就更有利于性能。

对东亚文字一如既往被 GB18030 吊打。

没有纯文本这种概念。没理由在一个名叫“string”的类里存储仅 ASCII 或 ANSI 代码页编码的文本。

然而 std::string 其实彻头彻尾存的是二进制数据,还能在中间夹杂 \0 ……

况且,MultiByteToWideChar 不是最快的 UTF-8↔UTF-16 转换函数。

作为撸过目前为止还没有发现更快的 UTF-8 → UTF-16 解码实现的,我可以负责任地讲,实际上相当慢……虽然通常并不介意这点差距。

将文件存为无 BOM的 UTF-8 可以解决。MSVC 会认为文件已经在正确的代码页,就不会碰你的字符串。

(注:原文空白如此。)

然后 MSVC 很高兴地会瞎猜编码了……嗯,如果不介意打开 IDE 就弹窗警告的话。

实际情况是 MSVC 不保证会认为文件已经在正确的代码页。这和代码页的设置有关——如译文中指出的,DBCS 下这行不通。但是,MSVC 在此一定意义上是正确的;引起问题的原因是,它把无 BOM 的文本文件都视为需要使用代码页决定编码,因此自动损失了跨语言和代码页配置的编辑环境的可移植性。这是上文提到的关于 BOM 作为元数据进行强制验证的一个实例。(虽然这里默认使用对话框的处理方式经常使人恼火,这是一个用户体验问题,而不是数据处理流程上的设计的错误。)

如果用户要求这种处理方式仍然识别 UTF-8 ,那么需要把代码页设定为 65001 以要求提供 UTF-8 支持。但不提更改系统代码页配置是否会破坏其它 Win32 程序的假定而引起预料外的问题,这种利用代码页增加配置成本的做法本身就是一种鸵鸟政策而不值得提倡。(有谁有本事看一个全局变量不爽而把它设置为固定值就能总是当作不存在呢……)另一方面,如果代码页这种设计是 garbage ,那么就当 garbage in garbage out ,避免以任意形式构成依赖才更干脆。

如果你打算设计一个接受字符串的程序库,只要用简单、标准且轻量的 std::string 就好。

“接受”和“储存”不是一个概念。

这种观念显然过时了,被 string_view 吊打。

另外,至少还有 pmr::string ……好吧,文明用语,这里不需要多数落 C++ 来跑题。

如果你是 C 或 C++ 程序库作者,用 char* 和 std::string,默认 UTF-8 编码,并且拒绝支持 ANSI 代码页——因为这东西本来就不支持 Unicode。

负责的接口设计者应只在外部编码约定使用一致的普适编码,如 UTF-8 ,而避免作为实现细节的内部编码污染接口。至于 ANSI 代码页这玩意儿烂和 Unicode 无关。

12

这是我们基于经验和现实中程序员对于 Unicode 犯下真正的错误

然而对历史上导致这些设计的真正的错误却睁一只眼闭一只眼。

13

支持 Unicode 方可大幅提升用户体验。我们建议选择 UTF-8 而不是 UTF-16、GBK 或 GB18030 作为应用程序的默认编码。

这两句话连在一起,说得 GB18030 不是基于 Unicode 一样……(虽然 UTF-16 的部分应该不会被理解错。)原文并没有讲清的东西一笔带过,含义有些不清楚。

补充话题

  (因为非引用原文,所以使用不同的缩进。)

一般建议

  本文中指出的关于 UTF-8 的问题,或者更一般意义上的关于 Unicode 规格为基础的文本编码的适用性问题,都反映了许多用户对历史包袱的烂设计的容忍的一些怪现状;要推翻这个有病的现实,如同解决其它能作为历史包袱和技术债的兼容性问题来讲,当然并不可能是很容易的。在适合使用 UTF-8 或者 UTF-8 虽然有明显缺陷但比其它替代品来讲仍有足够大价值的场景,如已使用 UTF-8 ,也未必需要改变。至于何谓“足够大”,这就是开发者能发挥艺术特长的领域了。

  一般地,一种应对文本编码问题合理的策略是:区分需要互操作(外部系统交互和持久化需求)和其它需求的场景,对应选定外部编码内部编码并显式地区分边界。

  注意,这里的具体编码由需求决定;UTF-8 因为既有的广泛的互操作支持以及变长编码的缺陷在互操作情形下往往相对不重要的原因,可能是个好的外部编码的选项。然而不顾需求武断地混淆这种边界,或者推脱明确这种边界的责任,有可能造成新的兼容性灾难,或者根本就不可能合理地实现。《UTF-8 遍地开花》中的“结论”一章不加前提的所谓“简朴性更加重要”,就是这个意义上的一个反面教材。这种策略在下文的具体选择中直接产生冲突:所谓“始终以 UTF-8 输出文本文件”,如果需求就是实现 iconv 这样的程序该怎么办?(在要求非 UTF-8 的文件输出中假装操作的是 UTF-8 内容?)

  诸如此类肤浅的教条对任何有足够多真实设计(都未必需要编码)经验的开发者来讲,连构成误导的资格都没有,因此不足以有必要在正文中逐条批驳;但在一些尊重需求的态度和价值判断的方法上的问题上,仍然需要再强调一遍:

  负责确定解决方案设计的用户(开发者)必须明确,妥协是一种变通,而不是问题的完全的解。真正的解决方案对应的是明确被作为问题解决的需求而提出的设计;实现细节不应该通过不健全的解决方案的抽象泄漏而显得更正确。

深层问题

  进一步地,这样的问题(及对其的漠视),在开发者做出选择时之前已经存在。在明确了需求和现存问题之后,有个本应当自然的疑问往往被忽略了——现有的不理想的方案为何如此?有的开发者可能想过这样的问题,但因为对背景问题的来源了解不够全面而无所适从(确定不了合理的方案而胡乱选择,或者提出新的方案却使现状更加混乱);另一些开发者,很遗憾地,屈从了妥协了的“主流”方案外,还随大流地放弃了在此思考的机会。(这也是随着时间推移此类兼容性问题越来越棘手的原因之一。)

  细节是魔鬼,但逃避不会改变现实

  一个典例是先前提到过的关于代码页的问题。真要批判一番的话,系统性地烂不仅仅是代码页,而是在系统相关的 API 中普遍地包括以下几类设计缺陷:

  • 控制台文本接口;
  • 图形用户界面交互的文本接口;
  • 混合使用直接 API 的互操作问题。

  毫无疑问,这里应当批判的首要的是像 Windows 这样对文本编码的系统性解决方案和接口设计的缺陷(更确切地说,这主要是包含兼容 Win16 历史包袱风格的 Win32 设计的问题——虽然严格意义上 Windows NT 执行体层次上也不干净,但绝大多数开发者都不会去依赖;鉴于这不是本文讨论的主要内容,这里点到为止)。关于文本的可编程接口设计的混乱导致用户(包含开发者和非开发者的最终用户)无所适从,并且造成了一些低级的用户体验问题(如相当长一段时期 Powershell 中 chcp 65001 会直接崩溃)和兼容性/可移植性灾难,这看似因为历史原因(例如,当年根本没有 UTF-8 而选择了 UCS-2 ,后来强行改用不伦不类的 UTF-16 这样的奇葩演进路线)而情有可原;但和 Unicode 的一些设计失败之处一样,许多问题并不是只是设计自身的问题,还有开发者不当应用的助纣为虐。后者在加强兼容性包袱的效果的意义上,可以说是一丘之貉,甚至有些开发者对这种状况产生了莫名的斯德哥尔摩综合症。比如,因为需要在 Win32 上编码,所以就要以 VC++ 这样的低质量 C++ 实现(至少几年前一概如此)和 Win32 消息队列之类的糟粕出发来考虑“扩展”现有实现的工作量,全然没想到一些简单的既定事实:

  • 健全的可移植设计,通常应使用更不容易造成可移植性等关键可行性问题的方案作为基础,而不是反过来事倍功半。
  • 偿还残疾实现方案的技术债就应该是这些开发者的分内事。(是你接了这个活。你不入地狱,谁入地狱?)

  另一类细节障碍是对实践的不够熟悉。原文的作者显然不够明白 C 和 C++ 的麻烦。有几个典型的基本认知偏差使这篇文章里的建议(注意虽然有一章名义上是针对 Windows 文本处理,但因为至少关心可移植性而不全是)被削弱,甚至本身也多少变成了反面教材:

  • 没有充分认识到 wchar_t 自身不指定背后的字符集和编码上的非确定性(因此不适合可移植地作为 Unicode 字符集或编码中的字符表示)。
    • 实际上,ISO C++ 中使用 wchar_t 来表示的字符的范围和表示方式实质上一直是未指定(unspecified) 的,因为只有很少的规则规定了对应的对所谓的宽字符集(wide-character set) 的要求。ISO C 的做法也大体和 ISO C++ 类似,而且通常更松垮——如 wchar_t 不是内建的“字符类型”,而是标准库提供的某个整数类型的 typedef 别名。
    • 这里的“宽字符集”的要求实质上少得过分,以至于允许符合(conforming) 标准的实现实际上不用支持“宽”字符集。
      • 这使表示“宽”字符可能词不达意,直接让可移植性成了极端的空话—— Android NDK 早期版本(以及之后早期的 API level )使用和 char 同等表示范围的 wchar_t 就是个(大概)臭名昭著的例子。
      • 即便没有那么极端,所谓的宽字符具体有多“宽”,也是不确定的。例如,Windows 使用 2 字节 16 位的 wchar_t ;而 Linux 和其它一些系统(包括改掉了 1 字节的 Android )使用 4 字节的 wchar_t 。这在字符表示的值上就不可能保证可移植。
    • 造成这个问题的根本是宽字符集本来就不保证是 Unicode ,即便实际实现通常都使用 Unicode 。但即便都是 Unicode ,仍然不保证使用相同的编码。
      • 例如 4 个字节的 wchar_t 使用的基本都是 UCS4/UTF-32 ,这在二进制意义原则上除了字节序引起的差异外可以视为等价。
      • 但 Windows 早期即支持单一的 Unicode(实际是 UCS-2LE ,当时没有 UTF-8 ),大约在 Windows 2000 才开始(NT API 和部分 Win32 API 中)支持 UTF-16 (都是小端序 LE 编码)。更全面完整的 Unicode 支持(如 WM_UNICHAR 消息)至少是 Windows XP 以后的事。
    • 这也是 ISO C++ (以及 ISO C )需要 char16_tchar32_t 这样比 wchar_t 提供更多位宽要求的类型来支持 Unicode 的主要理由。但即便知道具体环境,也不能彻底替代。
      • ISO C++ 中的严格别名(strict aliasing) 规则使 wchar_t 在严格的可移植性意义下不能通过 reinterpret_cast 的常规 type punning 转换为其它实际允许在二进制意义上兼容的类型(如 Windows 下的 char16_twchar_t ),否则会引起未定义行为——而这已被流行的实现实际实现使用了。C++20 本来可能有希望能通过 [[may_alias]] 变通一下……好在大多数这种情形 C++ 的实现同时都是 C 的实现,而 ISO C 中的 char16_t 也好 wchar_t 都是整数类型的别名而不是单独的类型,恰好不受严格别名的约束(即便 ISO C 也有微妙不同但类似的规则),因此编译器(理论上)不能随便进行跨二进制翻译单元的优化——原则上它不能知道这里的源代码应该遵循 ISO C 还是 ISO C++ 的规则。
      • 库的支持残缺。如 ISO C++ 的 std::basic_string 尚且对 wchar_tchar16_t 之类有比较公平的支持,<iostream> 之类,除了 charwchar_t 外基本就没法用。(C 标准库也大致上类似。)
  • 编码上的“尽早转换”是只使用一种统一编码的策略。
    • 以损失少量可移植性代价,这很大程度是可行的,但没有充分理由说明直接使用 UTF-8 就更可靠。(反而还有更多潜在的问题。比如说……确定被支持的 Unicode 的版本是什么了吗?)
    • 不区分内部编码和外部编码适用的不同场景可能会使移植更麻烦。
  • 抛开和主题没有直接关系的对使用 fopen 和 iostream 的评价,至少使用 <fstream> 支持宽字符串的路径遗漏了相当多的细节而显得不可靠。
    • 如原文提到的,扩展具有可移植性缺陷,但这根本不是 MSVC 的问题。这里的路径支持问题是 ISO C++ 长期以来的一个缺陷 ,最终在 ISO C++17 解决
    • 这依赖标准库实现而不是编译器。Windows 上不只有 MSVC ,使用的标准库也未必是同一个。一个不需要依赖 C++17 的不同的更多实例参见这里。老实说,MSVC 的标准库扩展用起来还算是比较省事的,虽然有更麻烦的内部 API 不稳定的问题
  • Win32 中不依赖 UNICODE 宏的做法原则上是合适的,但是:
    • 至少到现在,微软的古董教条已经没有那么大的影响力。
      • 原文开头暗示使用 UNICODE 在 Win32 中使用 Unicode 是必须的。这是技术上错误的。如同在之后的关于 Windows 的建议隐含的,使用 W 版本的函数实际上并不需要依赖 UNICODE 宏的定义。注意 Win32 SDK API 通过 _T 和配套的宏的切换所谓 MBCS/Unicode 函数调用的整体机制是,没有 AW 后缀的宏名根据“是否使用 Unicode ”被替换为 A 或者 W 的版本的函数名。仅仅是因为不期望的替换,而不管是什么后缀,都是足够的 #undef 这些宏的理由。相比之下,类似 _T 这样的作为 ISO C 和 ISO C++ 的保留名称本身就是可移植代码应该避免使用的。
      • 微软在 VS2013 的时候就 deprecated 掉了 MFC 的 MBCS 版本支持(应该是考虑大部分不再使用非宽字符版本的 API 以及安装体积的问题),虽然还可以单独另外安装。不过,这反倒引起了困惑
    • 漏了应当使用 ::OutputDebugStringA(因为 W 的版本没啥用)这样的特例。
    • 默认使用更基本的 W 版本 Win32 API 有个更重要的理由是很多文件相关的 API 中,非 W 版本的支持是次等的,在功能上有更多的限制——如路径长度问题。
    • LPWSTR 这样的类型名称一定意义上是冗余的。为什么是 LPWSTR 而不是 PWSTR ?(简单点说,这是因为体系结构相关的 Win16 余孽……)如果不想要这样的无关紧要的问题分心,还不如 wchar_t* 省事;后者也显然具有更少的头文件依赖和字面上更好的可移植性。(当然,大概输入起来真的是 LPWSTR 快一点……LPCWSTRconst wchar_t* 的差距会更大。只是,为此牺牲可读性,或者不得不强调“这就只是 Windows 专用代码”,看上去都不怎么值。)

  当然,并不能指望所有开发者都是关于这类问题的专家;或者说,大多数开发者都不够了解问题的全貌。这从此类选型上的迷惘普遍存在于包含语言标准库的设计的现状中就可见一斑——仅仅考虑现在 ISO C++ 对 Unicode 支持的混乱情形以及教学上的问题(所谓 teachability ),就实在不容乐观。(不信?不用说别的,光考虑 char16_tchar32_t 相对 wchar_t 的可用性,以及如何使用标准库应对仅仅涉及 UTF-8 和其它 Unicode 编码之间的转换问题,就够麻烦的了。)这不是区区一个 UTF-8 遍地开花就能解决的问题。

  就 C++ 而言,这种支持上的问题甚至是深入语言核心特性的,并且已经显现了实践上的琐碎的麻烦。有用户指出,使用 C++11 引入的 u8(字符串)字面量前缀能解决无法明确编码的问题。这通常的确是个好的实践,但可能是因为这种实践不够遍地开花的关系,似乎鲜少有用户知道这种字面量在 C++20 的扩展(引入 char8_t 类型)后造成新的兼容性问题;尤其需要注意的是,核心的向后兼容问题是明确被注意到但被放弃支持的。作为所谓 UTF-8 子类型的合理使用的设计策略,其理由仅仅是为了避免所谓 mixing of UTF-8 data with non-UTF-8 data ,是很没逻辑的——难道用了这种设计,就能自动地使原本用 char 的既定的(大多数用户因为没别的现成专用的类型而默认会选择的)混用场景变成了不混用的足够理想的源代码么?而根本上,不论 UTF-8 字符是否真的应该适合使用专用的名义的(nominal) 数据类型来表示,它总是原本表示窄字符的 char(作为事实上“不确定执行字符集编码”的字符的表示类型)的子类型,而不会是反过来的关系,因此排除原始提案中的隐式转换后,有理由认为新的 char8_t 名义类型的语义在设计上根本不完整。更讽刺的是,如果一开始就不用 u8 前缀,而通过约定源代码使用的源字符集来强制 UTF-8 编码这样的策略,反倒不会有这种(本不必要的)基本不得不修改带有 u8 前缀的源代码才能确保完全兼容 C++20 的问题(还能适合非 UTF-8 编码)。这类细碎的问题和先前已经提及的各种 Unicode 方案固有的缺陷一样层出不穷,始终体现了在复杂现实上的头脑简单实现所谓的“简朴性”希望渺茫;下游用户是否有余裕来讨论“更加重要”也因此愈加可疑。

其它选项

  就“做正确的事”的意义上,选择 Unicode 衍生的方案本质上是一种妥协。这是因为更全面准确、立足于实现方案虽然不是不存在(如 Citrus Project ),但在 ISO 10646 兼容框架之外确实仍然普遍缺乏可用的实现。考虑到现实的工作量(尤其是整理字符集的问题上),使用完全不考虑 Unicode 编码兼容性的作为文本外部编码,是普遍欠缺实用意义的。即便如此,考虑不同编码的性质和不同的需求,UTF-8 仍然不是唯一的选项——其它基于 Unicode 的编码可能更合适。

  首先,不考虑是否基于 Unicode ,某些编码的使用场景要求文本最终被处理为便于一次(可能随机)访问的数据单元,典型实例如通过编码查找字体内的字形——文本中对应特定字形的编码通常被转译成特定的字符集的码位来作为字体支持的查找表的索引。这种字符集(现在往往是兼容 Unicode 的子集)使用变长或者定长编码通常都能满足需求,但使用变长代码频繁转换需要更大的开销,在支持如图形化文本编辑这样的场合是非优化且易错的实现方式——不如先转换文本片段为定长的内部编码(虽然对查找操作来说,这其实是一种外部编码)表示,再行处理。UTF-8 和 UTF-16 这样的变长编码就不甚符合这类场景的需要。强行使用变长编码容易引入接口设计上的复杂性,如 Win32 的 WM_UNICHAR 消息(虽然就现实需求中需要支持非 BMP 字符的频率来讲,这样的设计似乎也不算很不合理)。

  排除关心是否变长的内部编码,GB18030 是一个最典型的可替代 UTF-8 的例子:它同属变长编码,且能提供 UTF-8 支持的主要特性(虽然实现的广泛支持程度仍然可能是问题)。

  注意 虽然只有译文(第 13 章)提到了关于 GB18030 的观点,但这里的忽略 GB18030 这种有代表性(由国家标准化组织而不是国际组织维护,对特定字符集子集优化)的变体,本来就是一个选型上的技术缺陷——特别正文存在一章单独讨论亚洲文本的空间效率却仅考虑比较 UTF-16 和 UTF-8 的时候。英文文本在这一章中甚至还链接到了中译文章,显然没理由不注意到“亚洲文字”在这里在乎的是什么。(当然,标题上打倒 UTF-16 的目的还是基本达到了。)

  当然,GB18030 并非比无条件地 UTF-8 合适。存在一些明显的、合理的动机使开发者放弃支持 GB18030 而使用 UTF-8 ,或者至少优先考虑支持 UTF-8 :

  • UTF-8 是被更广泛支持的(对外部编码来讲这尤其重要)。
    • 有意地只使用 GB18030(而拒绝支持 UTF-8 )许多情况下并不是个实用的选项。
  • 并不是所有软件都需要满足中华人民共和国法律规定的强制标准。
    • 允许只选用 UTF-8 支持能减少复杂性。

  但是,不预设更特定需求的前提下,以下论点是站不住脚的:

  • “东亚文字不常见。”
    • 至少在国际互联网上随机文本来源的国际化场景中,CJK 字符未必如 ASCII 子集的字符常见,但其中占据 3 个八元组的汉字和标点符号几乎总是比 UTF-8 支持的占据 2 个八元组码位表示的字符更常见。背后的理由是,用户基数实在差太多了……
    • 基于 Unicode 的编码相当程度地(通过 unified ideographs )改善了不同东亚文字共用字形微妙的不同和编码兼容性差的问题。这是潜在的各自为政的历史遗留编码无法解决好的问题。这样的问题体现了 Unicode 对东亚文字的实用性。反过来,其它(如西欧)的一些文字,因为没有那么多需要编码的文字单元,不使用 Unicode 也可能解决很大一部分问题(例如,使用 ISO 8859 系列)。(再极端点说,英文只使用 US-ASCII 照样玩的转儿,加上 UTF-8 直接兼容,无视 Unicode 都行……要没 BOM 之类的元数据,咋知道用 UTF-8 编码的英文文本到底算不算用了 Unicode 来编码?)结果是,世界上用户数较多的主要书面语言中,东亚文字才是最有必要使用 Unicode 杀手应用,在这个意义上反而是最“常见”的。
  • “UTF-8 具有更好的兼容性。”
    • 事实是,至少考虑中文简化字惯用历史编码的向后兼容性,GB18030 编码完全或近乎完全向后兼容 GB2312 及衍生的 GBKCP936 这样的常见实现;而 UTF-8 没有这样的支持。越是关心这里的兼容性,则 GB18030 较 UTF-8 的优势也越明显。
    • 和其它基于 Unicode 的编码相比,GB18030 也能基本实现兼容,因为原则上每个 GB 18030 标准版本规定的字符集都复用了 Unicode 字符集(的某个版本),作为 UTF 使用的 GB18030 和 UTF-8 编码主要差异是码位映射规则的不同。
    • GB18030 使用的映射规则较复杂。但因为实现向后兼容性的需要,很大程度复用了被兼容的编码的码位分配。于是,GB18030 和 UTF-8 之间的映射,跟 GBK 和 UTF-8 兼容字符子集之间的映射类似,基本上都需要偏移映射表支持转换。因此,实现 UTF-8 和过时的编码的相互转换,并不会比实现 UTF-8 和 GB18030 互操作简单到哪去,而比混用 GB18030 与其基本向后兼容的过时编码更复杂。
    • 只有不考虑过时编码而只考虑 Unicode 的其它转换格式之间的兼容性,UTF-8 才可能有略好的兼容性支持,但这种差异实用上基本只能通过不同版本标准支持来体现,这意味着 Unicode 不同版本之间的微妙兼容性问题极有可能更大。
    • 在不考虑过时编码的场景,使用 UTF-8 到其它基于 Unicode 的编码之间的转换,因为省略较大的偏移查找表,而有若干性能优势。但这不是兼容性问题,而恰恰是放弃部分兼容性(和 GBK 等编码之间的向后兼容)才能实现的。
  • “不还是用 UTF-8 吗。”
    • 当然不是;对现实中绝大多数要折腾文本的需求,严格地用 UTF-8 ,可能性大概比完全不用 UTF-8 还小。这两点决定了原文标题意义上的论点在现实中走不通。虽然,要关注的重点自然不止这个。
    • 而且原文明明是鼓吹 UTF-8 不要 BOM 的……加了 BOM 算不算呢。

  另外,因为广泛的支持,UTF-8 可能比其它大部分编码有更多的较可靠的实现可供选择。这是一个合理理由,但不是纯粹的技术理由;这通常也不是需求要求的,因此根本上,这种理由也仅仅是妥协的一个实例。