(concat (symbol-name 'GET) (encode-coding-string "我" 'utf-8)) 几个 bytes?

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.


  1. Text Representations (GNU Emacs Lisp Reference Manual)

  2. 对于#x110000..#x3fff00这段码位的用途,猜测是用来处理类似GB18030对Unicode的奇怪映射规则用

  3. emacs/mule-conf.el at master · emacs-mirror/emacs · GitHub

  4. 事实上,像Java和C++的字符串设计也饱受诟病,Java11以前的char是一个UTF-16编码的双字节, 没法处理3字节的字符,因此被迫引入“代理对”这种概念。C++ STL的std::string则完完全全是一个字节流,对字符串相关的处理毫无帮助。而新出的语言则学乖的不少(Go除外)

  5. emacs/lread.c at master · emacs-mirror/emacs · GitHub

10 个赞