同一个可见的 é,可以在字符层写成 U+00E9,也可以是 U+0065 U+0301。选修实验先从合法的字符 "e" . chr(0x301) 做 Unicode::Normalize::NFC,验证结果等于 chr(0xe9);再经 Encode::encode('UTF-8', ..., FB_CROAK) 得到 2 个字节。本机 Unicode::Normalize 1.32 的正例 STDOUT same=1 bytes=2\n,STDERR 空、退出 0。字符的个数与 UTF-8 字节数不是同一个计量单位。

反例让 Encode::decode('UTF-8', pack('C', 255), FB_CROAK) 接收无效字节 FF:STDOUT 空,STDERR 有 UTF-8 映射失败,退出非零。先对不明来源字节做 NFC 并不能“修复”坏编码。文件中的源码字面量如要直接写非 ASCII,use utf8 只影响源码解析;原始日志读入仍必须显式解码,字符处理完后仍必须显式编码输出。

图:源码解析、输入解码与消息规范化

1
2
3
源码字面量 (use utf8)  -> 源码解析字符
TSV raw bytes -> decode(FB_CROAK) -> 字符 -> 可选 NFC -> encode -> 输出字节
| 无效字节拒绝,不进入 NFC

主线工具不默认 NFC 重写 message:规范化可能改变外部记录的字节身份,必须另立契约与金标后再改。补充实验 course-semantics.pl unicode 增加两条边界:decoded_chars=2 bytes=6 说明两个中文字符编码后是 6 个 UTF-8 字节;nfc_equal=1 codepoints=2 graphemes=1 说明组合式 é 在码点层是两个标量值,在字素簇层是一个用户可见字符。double_decode=error utf8_flag=1 则防止把已经解码出的字符标量再次当作原始八位字节解码。

[PATTERN] 字节、码点和字素簇是三个计量层。文件边界先处理八位字节和解码错误;字段内部若需要规范化,必须保留原始来源或另立输出契约;展示层若按“用户可见字符”计数,才考虑 \X 这类字素簇规则。主线工具继续不默认 NFC 重写 message,因为那会改变外部日志的字节身份。官方依据是 Perl 5.38.2 perlunicode、perluniintro、perlrecharclass 的 \X 说明,以及 Encode 模块文档。

练习:若将组合式 é 的 UTF-8 字节串直接按字节截成前两字节再 NFC,会怎样?可能截断合法码位,先解码与验证,再操作字符。正反例及金标见 examples/perl-language/evidence/E01-E08.md;参考 5.38.2 pod/perlunicode.pod、pod/perluniintro.pod、pod/perlfunc.pod 中的 use utf8 边界及本机模块文档。