Swift中字符串設(shè)計(jì)得很糟糕? 不存在的.
作為吃大白米飯直立行走的哺乳動(dòng)物, 我們對(duì)形形色色的文字已經(jīng)見(jiàn)怪不怪. 但對(duì)于吃進(jìn)去的是電, 只記得一堆01串的計(jì)算機(jī)來(lái)說(shuō), 理解并且能夠表示文字可不是件容易的事. 要讓計(jì)算機(jī)理解文字, 最簡(jiǎn)單的方式就是將文字轉(zhuǎn)變成它能夠直接處理的數(shù)字, 對(duì)于這種從文字到數(shù)字的映射, 我們稱其為編碼.
從 ASCII 講起
ASCII 是一套基于拉丁字母的編碼系統(tǒng), 它定義了127個(gè)常用字符, 包括26個(gè)基本拉丁字母, 阿拉伯?dāng)?shù)字和英式標(biāo)點(diǎn)符號(hào), 以及不可見(jiàn)的控制字符, 如空格, 換行等. 在計(jì)算機(jī)中, 只需要 7Bit 就可以表示下任意的 ASCII 字符, 相對(duì) 1Byte 而言, 還留出了一位用于表示其它字符, 為了利用好這一 Bit, 人們?cè)谥蟀l(fā)明出了眾多 8Bit 的編碼方式.
但是這些編碼方式之間互不兼容, 同樣的編碼在不同的編碼系統(tǒng)中代表著完全不同的兩個(gè)字符. 并且對(duì)于一些語(yǔ)言來(lái)說(shuō), 8Bit 所能表示的字符量實(shí)在太少了. 于是又出現(xiàn)了各個(gè)針對(duì)不同語(yǔ)言的編碼方式, 廣為人知的 GB 2312, BIG-5 就是針對(duì)中文而產(chǎn)生的中文字符集.
這樣雖然能夠表示所有的字符, 但是各玩各的總不是互聯(lián)網(wǎng)的玩法, 計(jì)算機(jī)也無(wú)法支持多語(yǔ)言環(huán)境. 于是, 為了解決傳統(tǒng)編碼方式的局限, Unicode 標(biāo)準(zhǔn)產(chǎn)生了, 它對(duì)世界上大部分的文字系統(tǒng)進(jìn)行了整理, 編碼, 提供了一套通用的解決方案.
Unicode 使用 21Bit 來(lái)表示字符, 221 = 2,097,152, 這個(gè)數(shù)量已經(jīng)大到可以表示整個(gè)人類(lèi)歷史上的所有字符了. 但截至目前, 也只有十萬(wàn)個(gè)出頭的編碼被使用. Unicode 為世界上幾乎所有的字符都定義了一個(gè)數(shù)字, 這個(gè)數(shù)字叫做碼點(diǎn), 用 U+xxxx
的形式書(shū)寫(xiě), xxxx
代表4到6個(gè)十六進(jìn)制數(shù).
'U+0041' <=> 'A'
'U+1F60A' <=> ':blush:'
需要注意的是, Unicode 只是定義了從字符到數(shù)字的映射, 卻沒(méi)有定義具體如何存儲(chǔ)這些信息. 倘若使用 2Byte 來(lái)存儲(chǔ)字符, 不足以表示所有字符信息, 使用 4Byte 則又會(huì)造成空間的浪費(fèi). 為了解決這一問(wèn)題, 又出現(xiàn)了多種針對(duì) Unicode 的實(shí)現(xiàn)方式.
UTF-8, UTF-16 和 UTF-32
UTF-8, UTF-16 和 UTF-32 是針對(duì) Unicode 的三種編碼方式,下面將一一介紹. 不過(guò)首先, 還是得簡(jiǎn)單了解一下 Unicode 的組成.
前面說(shuō)過(guò) Unicode 使用 21Bit 來(lái)表示字符, 在這其中又分為基本多文種平面(Basic Multilingual Plane,簡(jiǎn)稱BMP) 和補(bǔ)充平面, 每個(gè)平面擁有 216 = 65,536 個(gè)可表示字符, 又稱作碼點(diǎn). 它們的表示范圍見(jiàn)下表, 摘自 wiki 百科.
平面 | 始末字符值 | 中文名稱 | 英文名稱 |
---|---|---|---|
0號(hào)平面 | U+0000 - U+FFFF | 基本多文種平面 | Basic Multilingual Plane门坷,簡(jiǎn)稱BMP |
1號(hào)平面 | U+10000 - U+1FFFF | 多文種補(bǔ)充平面 | Supplementary Multilingual Plane,簡(jiǎn)稱SMP |
2號(hào)平面 | U+20000 - U+2FFFF | 表意文字補(bǔ)充平面 | Supplementary Ideographic Plane实夹,簡(jiǎn)稱SIP |
3號(hào)平面 | U+30000 - U+3FFFF | 表意文字第三平面(未正式使用[1]) | Tertiary Ideographic Plane脏嚷,簡(jiǎn)稱TIP |
4號(hào)平面至13號(hào)平面 | U+40000 - U+DFFFF | (尚未使用) | |
14號(hào)平面 | U+E0000 - U+EFFFF | 特別用途補(bǔ)充平面 | Supplementary Special-purpose Plane褒颈,簡(jiǎn)稱SSP |
15號(hào)平面 | U+F0000 - U+FFFFF | 保留作為私人使用區(qū)(A區(qū))[2] | Private Use Area-A安皱,簡(jiǎn)稱PUA-A |
16號(hào)平面 | U+100000 - U+10FFFF | 保留作為私人使用區(qū)(B區(qū))[2] | Private Use Area-B氛雪,簡(jiǎn)稱PUA-B |
UTF-16
實(shí)際情況來(lái)講, 65,536個(gè)字符已經(jīng)足夠覆蓋到我們?nèi)粘K佑|到的符號(hào)了, 這也是 UTF-16 使用 2Byte 作為字符存儲(chǔ)單元的原因, 也叫做碼元. 所有基本多語(yǔ)種平面內(nèi)的字符都可以用一個(gè) UTF-16 碼元表示, 而對(duì)于罕見(jiàn)的補(bǔ)充平面內(nèi)字符, 一個(gè)碼元是不夠用來(lái)表示的.
UTF-16 是變長(zhǎng)編碼, 在最開(kāi)始沒(méi)有補(bǔ)充平面的時(shí)候每個(gè)字符都由一個(gè)碼元表示, 等到 Unicode 標(biāo)準(zhǔn)提出了補(bǔ)充平面的概念后, 它為這些超出一個(gè)碼元表示能力的字符提供了一個(gè)映射關(guān)系, 使之映射到2個(gè)碼元, 并且計(jì)算機(jī)能夠分別字節(jié)歸屬單碼元還是雙碼元, 不會(huì)存在理解上的誤差.
在基本多文種平面中, 預(yù)留了 0xD800-0xDFFF 之間的碼位未被使用, 而補(bǔ)充平面內(nèi)的碼位都在 0x10000-0x10FFFF 這個(gè)范圍內(nèi), 如果用它們的碼位值減去 0x10000, 就剛好能得到一個(gè)位于 0x0000-0xFFFFF 范圍的數(shù)字, 這些數(shù)字可以被一個(gè) 20Bit 的數(shù)字表示. 該數(shù)字的前10位加上 0xD800想虎,就得到 UTF-16 雙碼元編碼中的第一個(gè)碼元, 該數(shù)字的后10位加上 0xDC00卦尊,就得到 UTF-16 雙碼元編碼中的后一個(gè)碼元.
/// 對(duì)于小于0xFFFF的即基本平面的字符,為兩個(gè)字節(jié)
U+8D9E = 0x8D9E ///對(duì)應(yīng)的二進(jìn)制格式為:10001101 10011110
/// 對(duì)出于輔助平面的字符
/// 對(duì)于U+1D306
(0x1D306-0x10000) / 0x400 + 0xD800 = 0xd834
(0x1D306 - 0x10000) % 0x400 + 0xDC00 = 0xdf06
// 最終兩個(gè)碼元都落在了保留區(qū) 0xD800-0xDFFF 內(nèi), 計(jì)算機(jī)讀到這個(gè)碼元的內(nèi)容就知道是雙碼元字符的一部分了
UTF-8
UTF-8 也是可變長(zhǎng)編碼, 使用1-4個(gè) Byte 來(lái)表示一個(gè)字符, 一個(gè)碼元只有 1Byte. 它定義了一個(gè)巧妙的編碼規(guī)則, 完全兼容了 ASCII. 對(duì)于 U+0000-U+007F 之間的 ASCII 碼, 用 UTF-8 表示就是 0x0xxxxxxx, 跟 ASCII 完全一樣的一個(gè)碼元.
對(duì)于大于 U+007F 的非 ASCII 碼, 則采用填格子的方式進(jìn)行編碼, 參照下表.
Unicode 字符: UTF-8 碼:
U+00000000 - U+0000007F: 0xxxxxxx ///表示ASCII
U+00000080 - U+000007FF: 110xxxxx 10xxxxxx
U+00000800 - U+0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
U+00010000 - U+001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
以':blush:'為例, Unicode 碼為 U+1F60A, 查表位于 U+00010000-U+001FFFFF 區(qū)間.
轉(zhuǎn)換成二進(jìn)制 11111011000001010, 依次填入所對(duì)應(yīng)的格子里, 得到 11110111 10110110 10000010 10100000, 就是該字符所對(duì)應(yīng)的 UTF-8 編碼.
UTF-32
UTF-32 更為簡(jiǎn)單粗暴, 一個(gè)碼元 4Bit, 完全能夠覆蓋到所有 Unicode 字符. 但由于空間上的低效, 一般不被使用.
Objective-C
OC 中的 NSString 對(duì)象實(shí)際上代表著使用 UTF-16 編碼的碼元數(shù)組, 而返回字符串長(zhǎng)度的 length: 方法則會(huì)直接返回碼元的個(gè)數(shù). 那么問(wèn)題來(lái)了, 并不是所有的 Unicode 字符都可以使用1個(gè)碼元表示, 因此 NSString 在這方面存在一些缺陷.
并且 Unicode 中還存在著組合字符這一現(xiàn)象, é 可以使用 U+00E9 來(lái)表示, 也可以使用 U+0065 (代表字母 e) 后跟一個(gè)尖音符號(hào) U+0301 來(lái)表示, 二者在輸出的時(shí)候表現(xiàn)一模一樣, 但是在獲取長(zhǎng)度方面一樣有缺陷.
NSString *string = @":grinning:";
NSLog(@"%@", @(string.length)); // 2
NSString *eString1 = @"\u00e9";
NSLog(@"%@ length is %@", eString1, @(eString1.length)); // é length is 1
NSString *eString2 = @"e\u0301";
NSLog(@"%@ length is %@", eString2, @(eString2.length)); // é length is 2
因?yàn)檫@個(gè)缺陷, 在隨機(jī)訪問(wèn), 遍歷, 比較等對(duì)字符串的操作一樣會(huì)遇到跟人類(lèi)直接感官不一致的問(wèn)題.
Swift 設(shè)計(jì)原則
為了解決這個(gè)問(wèn)題, Swift 采用了一種抽象的方式來(lái)存儲(chǔ)字符串, 開(kāi)發(fā)者根本不需要關(guān)心其到底是什么編碼. 開(kāi)發(fā)者對(duì) String 的訪問(wèn)和操作都是以 Character 為單位的, 而每一個(gè) Character 實(shí)例都代表一個(gè) 可擴(kuò)展字形群集(extended grapheme cluster, 該翻譯來(lái)源于極客學(xué)院 Swift 教程).
簡(jiǎn)單來(lái)說(shuō), 就是 Character 就代表著一個(gè)人類(lèi)肉眼所見(jiàn)的一個(gè)字符, 不管該字符是否由多個(gè) Unicode 字符組成. :umbrella: (U+2614 U+FE0F), é(U+0065 U+0301) 通通代表著一個(gè) Character. 這樣就避免了使用 count方法, 遍歷, 比較的時(shí)候產(chǎn)生與人類(lèi)認(rèn)知不相符的結(jié)論.
var sunString = "今天天氣真好:sunny:"
print("\(sunString) last character is \(sunString.last!)") // 今天天氣真好:sunny: last character is :sunny:
var rainString = "又特么下雨了\u{2614}\u{fe0f}"
print("\(rainString) count is \(rainString.count)") // 又特么下雨了:umbrella: count is 7
那么問(wèn)題來(lái)了, 為什么 String 不提供直接使用整數(shù)下標(biāo)進(jìn)行訪問(wèn)的接口, 而要使用 index(after:), index(before:) 這樣復(fù)雜的接口來(lái)生成訪問(wèn)下標(biāo)的接口呢?
原因有兩個(gè):
- 使用下標(biāo)訪問(wèn)字符串并不是一個(gè)必要的操作.
- 使用整數(shù)作為下標(biāo)不是一個(gè)正確的行為, 會(huì)讓開(kāi)發(fā)者認(rèn)為自己在操作一個(gè)線性的集合, 所有的訪問(wèn)操作都是常數(shù)級(jí)的復(fù)雜度. 但 Character 實(shí)際存儲(chǔ)并不一定是固定內(nèi)存, 使用整數(shù)下標(biāo)訪問(wèn)實(shí)際上伴隨著遍歷的過(guò)程, 線性復(fù)雜度. 而 index(after:), index(before:) 這樣設(shè)計(jì)接口不僅能讓開(kāi)發(fā)者直觀感受到訪問(wèn)的開(kāi)銷(xiāo), 并且在遍歷的時(shí)候連續(xù)調(diào)用 index(after:) 在上一位置接著掃描, 提高了運(yùn)行效率.
不過(guò)還是有些場(chǎng)景是編碼敏感的, 為此 Swift 同樣提供了訪問(wèn)各種編碼視圖的接口.
let string = "今天天氣真不錯(cuò):grinning:"
print("\(string.utf8.count)") // 25
print("\(string.utf16.count)") // 9
print("\(string.unicodeScalars.count)") // 8
Swift4
Swift4 作為一個(gè)改進(jìn)版本, 對(duì)字符串同樣進(jìn)行了一些提高易用性的修改(別想了, 不會(huì)讓你用整數(shù)訪問(wèn)下標(biāo)的).
-
多行字符串. Swift4 提供了支持多行的字符串字面量表達(dá)方式.
print( """ :oncoming_automobile: Test Drive -------------- Quickly try out any Swift pod or framework in a playground. Usage: - Simply pass a list of pod names or URLs that you want to test drive. - You can also specify a platform (iOS, macOS or tvOS) using the '-p' option - To use a specific version or branch, use the '-v' argument (or '-m' for master) Examples: - testdrive Unbox Wrap Files - testdrive https://github.com/johnsundell/unbox.git Wrap Files - testdrive Unbox -p tvOS - testdrive Unbox -v 2.3.0 - testdrive Unbox -v swift3 """ )
?
在之前的版本中, String 一直遵循著 collection 協(xié)議, 使得開(kāi)發(fā)者可以像操作集合一樣操作 String. 在 Swift4 中, String 直接被作為了 collection 類(lèi)型, 意味著開(kāi)發(fā)者能夠簡(jiǎn)單的把它當(dāng)做 a collection of characters.
-
新的生成 SubString 的 API.
let substring = string[index...]
結(jié)語(yǔ)
在C語(yǔ)言中, 字符串就是一串非0序列的 Byte 數(shù)組, 獲取長(zhǎng)度都需要整個(gè)遍歷字符串, 對(duì)于 Unicode 字符則提供了額外的 wchar_t 類(lèi)型.
而 Java, Objective-c 這些語(yǔ)言則將字符串看成一堆 2Byte 碼元的集合, 這跟當(dāng)時(shí)的環(huán)境有關(guān)系. Unicode 在1991年被提出, 當(dāng)時(shí)總計(jì)7,161個(gè)字符, 直至1999年也才收錄49,259個(gè)字符. 如今流行的一大批編程語(yǔ)言都是在這個(gè)時(shí)間點(diǎn)之前誕生. 等到了 2Byte 滿足不了字符展示需求的時(shí)候, 這些語(yǔ)言要再改變字符串的設(shè)計(jì)為時(shí)已晚.
幸運(yùn)的是 Swift 開(kāi)發(fā)者們可以享受到 Swift 對(duì)字符串設(shè)計(jì)所帶來(lái)的便利性, 開(kāi)發(fā)者不用再去關(guān)心各種 Unicode, 組合字符, 編碼格式. 字符就只是字符. 新上手的開(kāi)發(fā)者可能會(huì)對(duì)不能使用整形下標(biāo)訪問(wèn)字符串感到不習(xí)慣, 不過(guò)無(wú)所謂了, they will get used to it 不是蘋(píng)果的一貫作風(fēng)嘛. ┑( ̄Д  ̄)┍