multibyte string(下称string)是字符串抽象,里面保存的是 字符 (character). unibyte string(下称bytes)是字符串编码后在计算机里的存储形式,类似其他语言的字节流。
乍一看可能不清楚,用seq-into
转化为vector或者list就能看明白
(seq-into "我能吞下玻璃而不伤身体" 'vector)
;; => [25105 33021 21534 19979 29627 29827 32780 19981 20260 36523 20307]
(seq-into (encode-coding-string "我能吞下玻璃而不伤身体" 'utf-8) 'vector)
;; =>
;; [230
;; 136
;; 145
;; 232
;; 131
;; 189
;; 229
;; 144
;; 158
;; 228
;; 184
;; 139
;; 231
;; 142
;; 187
;; 231
;; 146
;; 131
;; 232
;; 128
;; 140
;; 228
;; 184
;; 141
;; 228
;; 188
;; 164
;; 232
;; 186
;; 171
;; 228
;; 189
;; 147]
string转换成vector后,每个元素代表了该字符的码点(在Unicode字符集里的位置)。而bytes转换为vector,其元素就是用调用encode-coding-string
时所指定的编码形式进行编码后的字节。
Emacs的string使用的编码是utf-8-emacs
,其他编码的文件我们在打开时先转码成字符,保存时再编码成字节流写回硬盘。该编码的编码方式与utf-8一致,编码“emacs”字符集。“emacs”字符集是“unicode”字符集的超集,开辟了#x110000..#x3fffff
来一些额外字符。[^1][^2][^3]其中#x3fff00..#x3fffff
是非ASCII编码范围的裸字节的字符表示形式(128-255
)
为什么要给裸字节赋予字符码位?我们知道,UTF-8编码并非稠密的,对于任意字节流,我们不一定能将其按UTF-8编码解析为合法字符串。这块额外字符定义就是为了给无法解析为Unicode character的漏网之byte一个身份(像暂住证),使string能正常生成。
或许看起来不太合理,我们更喜欢bytes和string能分开成两个类型(或许用#b"\200\201"
这样的reader syntax来表示bytes),对于非法byte转string我们或许希望直接error。但是别忘了Emacs是有40年历史的骨灰级软件,存在出于历史局限性上的设计不完美也无可厚非[^4]。
回到这个问题上,这个问题的本质就是concat
在连接string和bytes时,会把bytes强转成string,而且不经任何编码,因为Emacs reader在读取字符串时,会尽量读取成unibyte string(bytes)[^5],除非遇到真正的multibyte character(如汉字)。对于unibyte string是ASCII字符的情况,这样做是符合UTF-8编码的。而如果unibyte里含有raw byte,就会直接使用#x3fff00..#x3fffff
的raw byte码位。
再用seq-into
大法。
(seq-into (concat (symbol-name 'GET) (encode-coding-string "我" 'utf-8)) 'vector)
;; => [71 69 84 4194278 4194184 4194193]
(seq-into (concat "GET" (encode-coding-string "我" 'utf-8)) 'vector)
;; => [71 69 84 230 136 145]
在这里(symbol-name 'GET)
返回一个multibyte string,因为Emacs内部就是保存为multibyte string,因此大意了,没有转换。而4194278
4194184
4194193
都是一个22bit的数字,加上padding,约为3个bytes,因此string-bytes结果返回为9. 同理第二个因为是ASCII字符,默认读取成unibyte,concat后依然是unibyte,不转换为character,返回6.
-
对于
#x110000..#x3fff00
这段码位的用途,猜测是用来处理类似GB18030
对Unicode的奇怪映射规则用 -
事实上,像Java和C++的字符串设计也饱受诟病,Java11以前的char是一个UTF-16编码的双字节, 没法处理3字节的字符,因此被迫引入“代理对”这种概念。C++ STL的
std::string
则完完全全是一个字节流,对字符串相关的处理毫无帮助。而新出的语言则学乖的不少(Go除外)