本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。
不定長度字元
一開始,字串編碼這件事很簡單。ASCII碼是一組從0到127的整數,因為128 = 2 ^ 7,因此如果把它存在八個位元組中,還能多餘一位。所以字串中的每一個字元可以隨機檢索[1]。
但是對於非英語國家的人來說,他們需要的符號遠不是128個ASCII碼能表示的(比如漢字)。ISO/IEC 8859標準利用了空餘的第八個位,擴充了很多符號,但依然不夠。當我們把這八個bit位全部用上,但還是有些符號無法表示的時候,我們可以選擇繼續增加bit位數,比如用16個位來儲存字元,或者可以讓每個字元佔用的bit位是可變的。Unicode最初使用了2位元組的固定長度,這意味著它可以儲存2 ^ 16 = 65536個字元,不過目前看來依然不夠,但如果增加到4位元組,在通常情況下效率又太低。
在進一步學習之前,有必要理清楚Unicode編碼中的幾個概念:
-
字元:字元是抽象的最小文字單位,它沒有固定的形狀(比如
A
、€
、我
都是字元),字元沒有值。 -
字符集;字符集是字元的集合。比如所有漢字構成漢字字符集,還有英文字符集、日語字符集等等。
-
編碼字符集:這是一種特殊的字符集。它為每個字元分配一個惟一的數字。Unicode標準的核心是Unicode編碼字符集,比如字元
A
會分配一個數字0041
,Unicode中的數字總是使用16進位制。 -
程式碼點:英文是
Code Point
,它表示可用於編碼字符集的數字。程式碼點U+0041
對應的字元是A
。編碼字符集會定義程式碼點的取值範圍,但是在這個範圍內,並非每個數字(程式碼點)都有對應的字元。 -
編碼方式:編碼方式表示了從一個程式碼點到一個或多個程式碼單元的對映方式。常見的編碼方式有UTF-32、UTF-16、UTF-8。
-
程式碼單元:程式碼單元是每一種編碼方式下的最基本單元。UTF-32表示程式碼單元是32位,因為16進位制的
00000041
恰好也是32位,所以UTF-32編碼方式非常簡單:一個程式碼點對映到一個程式碼單元,且兩者值相同。UTF-16下,一個程式碼單元是16位,但這不表示00000041
一定對映成0000
和0041
。UTF-16編碼方式有自己的對映規則,UTF-8也是同理。
以字母A
為例,A
是英文字符集中的一個字元,它的程式碼點是00000041
,在UTF-32編碼規則下的程式碼單元是00000041
,UTF-16下的程式碼單元是0041
,UTF-8下的程式碼單元是41
。
?
是一個字元,它的程式碼點是U+10400
,UTF-32下的程式碼單元是00010400
,UTF-16下的程式碼單元是D801
和DC00
,UTF-8下的程式碼單元有四個:F0
、90
、90
、80
。
目前Unicode使用了可變寬度格式,這體現在兩個方面:
- 程式碼點對映到的程式碼單元數量可變。在之前的例子中可以發現一個程式碼點在UTF-8下可以對映成1~4個程式碼單元。
- 組成字元的程式碼點數量可變。可能存在多個程式碼點組合成一個字元的情況,這一點我們待會兒會看到具體的例子。
Unicode標量是另外一些程式碼單元,它們可以當做程式碼點來用(除了UFT-16的代理對以外)。在Swift中,標量用字串字面量"\u{xxxx}"
表示,這裡的xxxx是一個16進位制的數字。
之前我們說過,組成字元的程式碼點數量可變。也就是說使用者在螢幕上看到的一個字元,可能是由多個程式碼點組成的。大多數處理字串的程式碼一定程度上都沒有注意到Unicode可變寬度的特性,這可能會導致一些bug。Swift在字串時,花費了巨大的努力,儘可能正確的使用了Unicode。至少會在有錯誤時讓開發者知道。這也付出了一定的代價,String型別並不是一個集合,而是提供了多種不同的視角來觀察字串,你可以把字串當做字元(Character)的集合,也可以當做UTF-8或UTF-16編碼下的程式碼單元的集合,或是Unicode標量的集合。Character
和另外幾個檢視的區別在於,它可以把若干個程式碼點組合成一個“字形叢集(Grapheme Cluster)”
出了UTF-16以外的所有檢視都無法通過下標隨機訪問,不同的檢視在處理大量文字處理時有快有慢,在本章我們會探索其背後的原因。我們還會了解一些處理文字和提高效能的技術。
字形叢集和規範等價
為了展示Swift和NSString處理Unicode字元的區別,我們來分析一下列印字元é
的方法。作為一個單個字元,它的Unicode程式碼點是U+00E9
。但它也可以表示為字母e
後面加一個́
(程式碼點U+0301
)。無論選擇那種表示方法,最終顯示的結果都是é
,對於使用者來說不僅字串相同,長度也相同,都是1。這就是Unicode中“規範等價(Canonically equivalent)”。
我們在Swift中舉一個具體的例子,這兩個字串的顯示效果完全相同:
let single = "Pok\u{00E9}mon"
let double = "Pok\u{0065}\u{0301}mon"
print(single, double)
// 輸出結果是“Pokémon Pokémon”
複製程式碼
還可以證明一下他們的字串變數時相等的,字元數量也相等:
print(single == double) // 輸出結果:true
print(single.characters.count == double.characters.count) // 輸出結果:true
複製程式碼
不過,如果切換成UTF-16檢視,就可以看出兩者的區別了:
print(single.utf16.count) // 輸出結果為7
print(double.utf16.count) // 輸出結果為8
複製程式碼
如果使用NSString
,不僅字元數量不同,字串本身也不相同:
let nssingle = NSString(characters: [0x0065, 0x0031], length: 2)
let nsdouble = NSString(characters: [0x00E9], length: 1)
print(nssingle == nsdouble) //輸出結果是:false
print(nssingle.isEqualToString(nsdouble as String)) //輸出結果是:false
複製程式碼
其中等號運算子比較的是兩個NSObject
型別的物件,它的定義是:
func ==(lhs: NSObject, rhs: NSObject) -> Bool {
return lhs.isEqual(rhs)
}
複製程式碼
這是因為在NSString
的比較方法中,只考慮字面量是否相等,不會考慮多個字元的組合結果是否是“規範等價”的。如果你真的想進行規範比較,那麼需要使用NSString
的compare
方法。啥,你不知道這個方法?不好意思,那你就等著以後的iOS開發和資料庫開發中不停地報錯吧。
直接比較程式碼單元的優點在於速度非常快,比用characters
快很多。比如:
print(single.utf16.elementsEqual(double.utf16)) //輸出結果是:false
複製程式碼
不僅僅是兩個字元可以拼接組合成一個,更多的字元也可以拼接。比如約魯巴語中有一個字元:ọ̀
,它中間是字母o
,上面是一個類似於漢語中第四聲調的字元:"`",下面則是一個點:"."。它有四種表示方法:
- 字母
o
和其中一個符號拼接後的符號,和另一個符號拼接。這有兩種方法 - 三個字元分別拼接,其中
o
位於開頭,後面兩個字元的順序可以對調。這又是兩種方法。
我們用程式碼表示:
// U+6F是字母o,U+300是第四聲,U+323是"."
let chars: [Character] = [
"\u{1ECD}\u{300}", // U+1ECD是U+6F和U+323的拼接結果,等價於:(o + .) + 第四聲
"\u{F2}\u{323}", // U+F2是U+6F和U+300的拼接結果,等價於:(o + 第四聲) + .
"\u{6F}\u{323}\u{300}", // 等價於:o + . + 第四聲
"\u{6F}\u{300}\u{323}", // 等價於:o + 第四聲 + .
]
for char in chars {
print(char)
}
/** 列印結果:
ọ̀
ọ̀
ọ̀
ọ̀
*/
複製程式碼
事實上,這種聲調符是可以無限新增的,不過長度依然是1:
let many = "\u{1ECD}\u{300}\u{300}\u{300}\u{300}"
print(many.characters.count) // 輸出結果:1
print(many.utf8.count) // 輸出結果:11,U+1ECD在UTF-8下由3個程式碼單元組成,U+300由2個組成,11 = 3 + 2 * 4
print(many)
/* 字串輸出結果:
ọ̀̀̀̀
*/
複製程式碼
Emoji
Emoji表情不是很重要,但是很好玩。搞懂下面這個問題有助於幫助我們理解Unicode標量的拼接:
let emoji1 = "????????????"
let emoji2 = "???"
print(emoji1.characters.count)
print(emoji2.characters.count)
複製程式碼
如果你認為列印結果分別是6和3,那麼你就上當了。答案是1和3。回想一下之前ọ̀
這個字元,他有四種組成方法,但是細心的讀者可能會問,為什麼"\u{300}\u{6F}\u{323}"
這種寫法(也就是第四聲+o+.)不行?
這是因為在Unicode中,有些字元稱為基字元(base)。只有這種字元是可以向後擴充的,我們之前所說的字形叢集的定義是:“一個基字元,加上後面0或多個字元”。
所以,輸出結果是1而不是6的原因在於,Unicode規範中國旗是一個基字元,6個國旗拼接在一起會被認為是一個字形叢集,也就是依然是一個字元。而?並不是基字元,所以可以被正確識別為3個字元。
譯者注
[1]:考慮字串hello
,只要知道字元o
是第5個字元,因為每個字元的長度固定,都是8個bit位,所以立刻可以到第(5 - 1) * 8 = 32
個bit位去查詢字元o
。這就是原文中random access
的含義。如果每個位元組長度不定,則需要從頭開始遍歷。