Swift 中的字符串設(shè)計(jì)

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-16UTF-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è):

  1. 使用下標(biāo)訪問(wèn)字符串并不是一個(gè)必要的操作.
  2. 使用整數(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)的).

  1. 多行字符串. 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
            """
        )
    

    ?

  2. 在之前的版本中, String 一直遵循著 collection 協(xié)議, 使得開(kāi)發(fā)者可以像操作集合一樣操作 String. 在 Swift4 中, String 直接被作為了 collection 類(lèi)型, 意味著開(kāi)發(fā)者能夠簡(jiǎn)單的把它當(dāng)做 a collection of characters.

  3. 新的生成 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)嘛. ┑( ̄Д  ̄)┍

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市淌友,隨后出現(xiàn)的幾起案子煌恢,更是在濱河造成了極大的恐慌,老刑警劉巖震庭,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑰抵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡器联,警方通過(guò)查閱死者的電腦和手機(jī)二汛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)拨拓,“玉大人肴颊,你說(shuō)我怎么就攤上這事≡祝” “怎么了婿着?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)醋界。 經(jīng)常有香客問(wèn)我竟宋,道長(zhǎng),這世上最難降的妖魔是什么形纺? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任丘侠,我火速辦了婚禮,結(jié)果婚禮上逐样,老公的妹妹穿的比我還像新娘蜗字。我一直安慰自己,他們只是感情好脂新,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布挪捕。 她就那樣靜靜地躺著,像睡著了一般争便。 火紅的嫁衣襯著肌膚如雪级零。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,754評(píng)論 1 307
  • 那天始花,我揣著相機(jī)與錄音妄讯,去河邊找鬼。 笑死酷宵,一個(gè)胖子當(dāng)著我的面吹牛亥贸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播浇垦,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼炕置,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起朴摊,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤默垄,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后甚纲,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體口锭,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年介杆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鹃操。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡春哨,死狀恐怖荆隘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赴背,我是刑警寧澤椰拒,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站凰荚,受9級(jí)特大地震影響燃观,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜浇揩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一仪壮、第九天 我趴在偏房一處隱蔽的房頂上張望憨颠。 院中可真熱鬧胳徽,春花似錦、人聲如沸爽彤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)适篙。三九已至往核,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嚷节,已是汗流浹背聂儒。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留硫痰,地道東北人衩婚。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像效斑,于是被迫代替她去往敵國(guó)和親非春。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 字符是用戶可以讀寫(xiě)的最小單位护侮。計(jì)算機(jī)所能支持的字符組成的集合,就叫做字符集储耐。字符集通常以二維表的形式存在羊初。二維表的...
    劉惜有閱讀 8,117評(píng)論 2 14
  • 一個(gè)字符串 是一系列字符的集合,例如hello, world和albatross什湘。Swift的字符串是String...
    BoomLee閱讀 2,401評(píng)論 0 3
  • 字符集和編碼簡(jiǎn)介 在編程中常车拭Γ可以見(jiàn)到各種字符集和編碼,包括ASCII,MBCS,Unicode等字符集禽炬。確切的說(shuō)...
    蘭山小亭閱讀 8,498評(píng)論 0 13
  • 明星團(tuán)隊(duì)打造腹尖! 創(chuàng)始人哥哥Rory2012年開(kāi)始在哈佛學(xué)習(xí)心理學(xué)和經(jīng)濟(jì)學(xué)柳恐,弟弟Kieran2013年開(kāi)始在哈佛學(xué)習(xí)...
    Anthony007閱讀 1,704評(píng)論 0 0
  • 《吟曇》 七絕 平水韻 押陽(yáng)韻 不與群芳斗艷長(zhǎng) 更深月下散幽香 紅顏易逝催冰蕊 已使芬馨蕩繡房 ~~~~~~~~~...
    緬華資訊網(wǎng)閱讀 228評(píng)論 1 11