Swift中的String API

我發(fā)現(xiàn)很多人在使用 Swift 時(shí),都會(huì)抱怨 String API 很難用。它很難學(xué)習(xí)并且設(shè)計(jì)得晦澀難懂,大多數(shù)人希望它能采用其他語(yǔ)言的字符串(String) API 設(shè)計(jì)風(fēng)格。今天我就要來(lái)講一下為什么 Swift 中的 String API 會(huì)被設(shè)計(jì)成現(xiàn)在這樣(最起碼要解釋清楚我的看法),以及為什么我最終會(huì)認(rèn)為,就其基礎(chǔ)設(shè)計(jì)而言 Swift 中的 String API 是字符串 API 中設(shè)計(jì)得最好的。

一、什么是字符串?

在我們討論這點(diǎn)之前,首先需要建立一個(gè)基本的概念。我們總是把字符串想得很膚淺,很少有人能夠深入思考它的本質(zhì)。深思熟慮才能有助于我們理解接下來(lái)的內(nèi)容。
從概念上來(lái)說(shuō),什么 字符串呢?從表面上看,字符串就是一段文本。

"Hello, World"      //是字符串;
"/Users/mikeash"和"Robert'); DROP TABLE Students;--"    //也是字符串。

(順道講一下,我認(rèn)為不應(yīng)該把這些不同的文本表述概念看作是同樣的字符串類(lèi)型。人類(lèi)可讀的文本、文件路徑、SQL 查詢語(yǔ)句,以及其他所有在概念上講并不相同的東西,在語(yǔ)言表示層面上都應(yīng)該被表示成不同的類(lèi)型。我覺(jué)得這些概念上不同的字符串應(yīng)當(dāng)有不同的類(lèi)型,這也能大幅減少 bug 數(shù)量。盡管我并沒(méi)有發(fā)現(xiàn)有哪個(gè)語(yǔ)言或者標(biāo)準(zhǔn)庫(kù)做到了這點(diǎn)。)
那么在底層,這些常見(jiàn)的「文本」概念又是怎么被表示的呢?唔,得看情況。有很多不同的解決方法。
在很多語(yǔ)言中,字符串是用于存放字節(jié)(bytes)的數(shù)組(array)。程序所要做的就是為這些字節(jié)賦值。這種字符串的表示方法在 C++ 中是std::string類(lèi)型,Python 2、Go 和其他語(yǔ)言也是這樣。
C 語(yǔ)言對(duì)于字符串的表示就比較古怪和特殊。在 C 語(yǔ)言中,字符串是指向一串非零字節(jié)序列(sequence of non-zero bytes)的指針,以零字節(jié)位表示字符串的結(jié)束?;镜氖蛊鋵?shí)和數(shù)組一樣,但是 C 語(yǔ)言中的字符串不能包含零字節(jié)位,并且諸如查詢字符串長(zhǎng)度這樣的操作需要掃描內(nèi)存。
很多新語(yǔ)言把字符串定義成了一串 UCS-2 或者 UTF-16 碼元(code unit)的集合。Java、C# 還有 JavaScript 是其中的代表。同樣,在 Objective-C 中也使用了 Cocoa 和NSString。
這可能是一個(gè)歷史遺留問(wèn)題。Unicode 在 1991 年被提出時(shí)(譯者注:1991 年 10 月發(fā)布 Unicode 1.0.0),當(dāng)時(shí)的系統(tǒng)都是 16 位。很多流行的編程語(yǔ)言在那個(gè)時(shí)代被設(shè)計(jì)出來(lái),并且將 Unicode 作為字符串的構(gòu)成基礎(chǔ)。在 1996 年,Unicode 在 16 位系統(tǒng)上經(jīng)歷了爆發(fā)性的增長(zhǎng)(譯者注:1996 年 7 月發(fā)布了 Unicode 2.0,字庫(kù)從 7161 個(gè)字元變成了 38950 個(gè)),這些語(yǔ)言再要改變字符串的編碼方式已為時(shí)已晚。這時(shí),由于 UTF-16 的編碼方式能夠?qū)⒏蟮臄?shù)字編碼為一組 16 位碼元的集合,因此將字符串視為 16 位碼元序列的基本想法就這樣延續(xù)了下來(lái)。
這種想法的一個(gè)變體就是將字符串定義成 UTF-8 碼元序列,其中組成的碼元是 8 位的??傮w上來(lái)說(shuō)和 UTF-16 的表示方法很接近,但是對(duì)于 ASCII 字符串來(lái)說(shuō),能夠有更加緊湊的表示空間,而且避免了在傳遞字符串進(jìn)入函數(shù)時(shí),由于這些函數(shù)只接受 C 語(yǔ)言風(fēng)格類(lèi)型(也就是 UTF-8 字符串)而導(dǎo)致的轉(zhuǎn)換。
也有些語(yǔ)言將字符串表示為 Unicode 碼位(code point)指向的一段字符序列。Python 3 中就是這么實(shí)現(xiàn)的,在很多 C 語(yǔ)言實(shí)現(xiàn)中也提供了內(nèi)置的wchar_t類(lèi)型。
簡(jiǎn)短概括一下,一個(gè)字符串通常情況下會(huì)被當(dāng)做某些特定 字符(character)的序列,其中字符通常是一個(gè)字節(jié),或者是一個(gè) UTF-16 碼元,又或者是一個(gè) Unicode 碼位。

二、問(wèn)題

將字符串表示成一段連續(xù)「字符」的序列的確很方便。你可以把字符串看作是數(shù)組(array)(通常情況下就個(gè)數(shù)組),這樣就很容易獲得字符串的子串、從字符串頭部或者尾部取出部分元素、刪除字符串的某部分、獲取字符總數(shù),等等。
問(wèn)題是我們身邊遍布著 Unicode,而 Unicode 會(huì)讓事情變得很復(fù)雜。簡(jiǎn)單看一個(gè)字符串的例子,看一下它是怎么工作的:

aé∞??

每一個(gè) Unicode 碼位都有一串?dāng)?shù)字(寫(xiě)作 U+nnnn)和一個(gè)供我們看得懂的命名(某種原因使用全大寫(xiě)的英文字母表示),這樣我們更容易討論單個(gè)字符所表示的內(nèi)容。對(duì)于上面這個(gè)特定的字符串,它包括了:

U+0061 LATIN SMALL LETTER A
U+0065 LATIN SMALL LETTER E
U+0301 COMBINING ACUTE ACCENT
U+221E INFINITY
U+1D11E MUSICAL SYMBOL G CLEF

讓我們從字符串的中間移除一個(gè)「字符」。對(duì)于這個(gè)「字符」,我們嘗試用 UTF-8、UTF-16 和 Unicode 三種不同的字符編碼方式來(lái)講解。
首先將這個(gè)「字符」看作是一個(gè) UTF-8 字符單元。這個(gè)字符串在 UTF-8 下看上去長(zhǎng)這樣:

61 65 cc 81 e2 88 9e f0 9d 84 9e
-- -- ----- -------- -----------
 a  e   ′      ∞          ??

其中的U+0301轉(zhuǎn)化為cc 81的過(guò)程如下:

03 01:        11|0000|0001
cc 81:  1100|1100|1000|0001  
借cc的前2位11表示第二類(lèi)劃分。3、4位的00的位置表示由多少個(gè)字節(jié)組成,此處0的位置為2。
借81的10表示為第三類(lèi)劃分。

我們來(lái)移除第 3 個(gè)「字符」,即第三個(gè)字節(jié)(cc)。結(jié)果是:

61 65 81 e2 88 9e f0 9d 84 9e

這個(gè)字符串已經(jīng)不再是個(gè)合法的 UTF-8 字符串。UTF-8 的字符編碼有三類(lèi)。對(duì)于那些0xxxxxxx表示的,即由 0 開(kāi)頭的,會(huì)被表示為 ASCII 字符,單獨(dú)歸為第一類(lèi)。那些看上去形如11xxxxxx的,表示一個(gè)多位序列,長(zhǎng)度由第一個(gè) 0 的位置決定。第三類(lèi)表示成10xxxxxx,說(shuō)明一個(gè)多位序列的剩余部分。cc(譯者注:即11001100,劃分在第二類(lèi)。其中第一個(gè) 0 出現(xiàn)在從 0 開(kāi)始計(jì)數(shù)的第 2 位,故整個(gè)多位序列由兩個(gè)字節(jié)組成)表示了一個(gè)多位序列的開(kāi)始,長(zhǎng)度是兩個(gè)字節(jié),81(譯者注:即10000001,劃分在第三類(lèi))表示了這個(gè)多位序列的尾部。如果移除了cc,那么剩下的81將會(huì)被留在字符串中。所有 UTF-8 校驗(yàn)器都會(huì)拒絕識(shí)別這個(gè)字符串(譯者注:因?yàn)?1并不是合法的 UTF-8 頭部字符,只有第一類(lèi)和第二類(lèi)的字符是合法的)。如果我們移除了從第三位之后的任意一個(gè)字符,這個(gè)問(wèn)題依舊會(huì)發(fā)生。
那么如果是第二位呢?如果我們移除了這個(gè)字符,我們會(huì)得到:

61 cc 81 e2 88 9e f0 9d 84 9e
-- ----- -------- -----------
a    ′      ∞          ??

看上去這依然是個(gè)合法的 UTF-8 字符串,但是結(jié)果并不是我們所期待的那樣:

á∞??

對(duì)于人類(lèi)來(lái)說(shuō),在這個(gè)字符串中的「第二個(gè)字符」應(yīng)該是「é」。但是第二位上的字符僅僅是不帶語(yǔ)調(diào)標(biāo)記的「e」。這個(gè)語(yǔ)調(diào)標(biāo)記被看作是一個(gè)「連接字符(combining character)」,被單獨(dú)添加到前面的字符上。移除第二個(gè)字符僅僅是移去了「e」,導(dǎo)致這個(gè)語(yǔ)調(diào)標(biāo)記連接到了「a」字符上。
那么如果移去首個(gè)字符呢?最終結(jié)果是我們所想要的那樣:

65 cc 81 e2 88 9e f0 9d 84 9e
-- ----- -------- -----------
e    ′      ∞          ??

讓我們?cè)侔堰@個(gè)字符串當(dāng)做 UTF-16 編碼來(lái)看。在 UTF-16 編碼下,這個(gè)字符串看上去長(zhǎng)這個(gè)樣子:

0061 0065 0301 221e d834 dd1e
---- ---- ---- ---- ---------
 a    e    ′    ∞      ??

我們嘗試著移除第二個(gè)「字符」:

0061 0301 221e d834 dd1e
---- ---- ---- --------- 
  a   ′    ∞       ??

和上面在 UTF-8 中出現(xiàn)的問(wèn)題一樣,刪除了「e」,但是沒(méi)有刪除語(yǔ)調(diào)標(biāo)記,導(dǎo)致這個(gè)標(biāo)記附在了「a」上面。
那么如果刪除第五個(gè)字符呢?我們得到了如下的序列:

0061 0065 0301 221e dd1e

和不合法的 UTF-8 編碼是類(lèi)似的問(wèn)題,這個(gè)序列也不再是一個(gè)合法的 UTF-16 字符串。序列

d834 dd1e

形成了一組代理對(duì)(surrogate pair),指兩個(gè) 16 位的單元用于表示一個(gè)超過(guò) 16 位的碼位(譯者注:具體計(jì)算參考 Wiki)。而讓代理對(duì)中的一部分單獨(dú)出現(xiàn)在字符串中是非法的。在 UTF-8 中通常會(huì)出錯(cuò),而在 UTF-16 中這種狀態(tài)會(huì)被忽略。例如,Cocoa 會(huì)將這個(gè)字符串渲染成這樣:

aé∞?

(譯者注:即平時(shí)出現(xiàn)的亂碼現(xiàn)象。)
那么如果一個(gè)字符串被表示成一串 Unicode 碼位序列呢?字符串看上去是這樣的:

00061 00065 00301 0221E 1D11E
----- ----- ----- ----- -----
  a     e     ′     ∞     ??

對(duì)于這種表示方式,我們可以移除任意一個(gè)「字符」而不會(huì)導(dǎo)致產(chǎn)生一個(gè)非法的字符串。但是連接語(yǔ)調(diào)標(biāo)記的問(wèn)題依然存在。移除第二個(gè)字符將會(huì)是這樣:

00061 00301 0221E 1D11E
----- ----- ----- ----- 
  a     ′     ∞     ??

即使使用這種表示方法,我們也無(wú)法確保結(jié)果的正確。
這些通常不是我們能夠簡(jiǎn)單想到的問(wèn)題。英語(yǔ)是鮮有的幾種僅使用 ASCII 字符就能表示的語(yǔ)言。你肯定不想把求職時(shí)的簡(jiǎn)歷(Résumé)改成「Resume」吧!一旦超出 ASCII 字符集,這些荒謬的錯(cuò)誤就開(kāi)始出現(xiàn)了。

三、字素簇(Grapheme Clusters)

Unicode 中有個(gè)概念叫做字素簇(Grapheme Clusters),本質(zhì)上就是閱讀時(shí)會(huì)被考慮成單個(gè)「字符」的最小單元。在大多數(shù)表示方法中,一個(gè)字素簇就等價(jià)于一個(gè)單獨(dú)的碼位,但是也有可能會(huì)表示成包括語(yǔ)調(diào)標(biāo)記的一部分內(nèi)容。如果我們將上面的例子表示成字素簇的方式,那么很顯然會(huì)是這樣的:
a é ∞ ??
移除任意一個(gè)作為字素簇的單元,留下的內(nèi)容都會(huì)被認(rèn)為是合情合理的。
注意到在這個(gè)例子中,并沒(méi)有任何的數(shù)字等值 (numeric equivalents) 存在。這是因?yàn)榕c UTF-8、UTF-16 或者普通的 Unicode 碼位不同,單個(gè)數(shù)字無(wú)法在一般情況下表示字素簇 (grapheme cluster) 。所謂字素簇,指的是一個(gè)或多個(gè)碼位的序列集合。一個(gè)字素簇通常會(huì)包含一個(gè)或兩個(gè)碼位,但是某些情況下(比如 Zalgo 中)字素簇中也可能會(huì)包含大量的碼位。例如下面這個(gè)字符串:e?????????????
這一團(tuán)亂七八糟的字符串包括了 14 個(gè)不同的碼位:

+ U+0065+ U+20DD+ U+20DE+ U+20DF+ U+20E0+ U+20E3+ U+20E4+ U+20E5+ U+20E6+ U+20E7+ U+20EA+ U+A670+ U+A672+ U+A671

所有的這些碼位單元都表示一個(gè)單獨(dú)的字素簇。
下面有個(gè)有趣的例子。有這樣一個(gè)包含瑞士國(guó)旗的字符串:????
這個(gè)標(biāo)記實(shí)際上包括兩個(gè)碼位:
U+1F1E8 U+1F1ED
。這兩個(gè)碼位又表示什么意思呢?

+U+1F1E8 REGIONAL INDICATOR SYMBOL LETTER C+ 
U+1F1ED REGIONAL INDICATOR SYMBOL LETTER H

Unicode 包含了 26 個(gè)「Regional indicator symbol」而不是將地球上的所有國(guó)家的國(guó)旗作為單獨(dú)的碼位。將 C 和 H 的兩個(gè)標(biāo)識(shí)符合起來(lái)你就能得到瑞士的國(guó)旗。將 M 和 X 合起來(lái)會(huì)得到墨西哥國(guó)旗。每個(gè)國(guó)旗都是一個(gè)單獨(dú)的字符簇,但是由兩個(gè)碼位組成,即四個(gè) UTF-16 碼元或者八個(gè) UTF-8 字節(jié)。

四、字符串 API 實(shí)現(xiàn)方式

我們發(fā)現(xiàn)字符串有多種理解方法,也有多種表示「字符」的方式。將「字符」當(dāng)做一個(gè)字素簇可能最接近人們對(duì)于「字符」的理解,但是在代碼中操作字符串時(shí),要依據(jù)語(yǔ)言環(huán)境來(lái)判斷所謂「字符」的含義。

當(dāng)在文本中移動(dòng)插入光標(biāo)時(shí),光標(biāo)經(jīng)過(guò)的字符就是指字素簇。當(dāng)為了保證文本滿足 140 字限制的推文時(shí),這里的字符就是 Unicode 碼位。當(dāng)字符串想要保存在限定長(zhǎng)度是 80 個(gè)字符的數(shù)據(jù)庫(kù)表中時(shí),這里的字符就是個(gè) UTF-8 字節(jié)。

那么當(dāng)你在實(shí)現(xiàn)字符串時(shí),如何來(lái)平衡性能、內(nèi)存使用和簡(jiǎn)潔代碼三者呢?
通常的回答是選擇一種標(biāo)準(zhǔn)化表示(canonical representation),之后在需要其他表示方法時(shí)進(jìn)行轉(zhuǎn)換。例如,NSString使用 UTF-16 作為其標(biāo)準(zhǔn)化表示法。整個(gè) API 基于 UTF-16 建立。我們可以這樣操作:

NSString *test = @"I Love ????";
for (int i = 0;i<test.length;i++) {
        unichar ch = [test characterAtIndex:i];
        NSLog(@"%c",ch);
}

輸出結(jié)果如下:

2016-03-04 12:19:51.849 NSString[1186:65935] I
2016-03-04 12:19:51.850 NSString[1186:65935]  
2016-03-04 12:19:51.850 NSString[1186:65935] L
2016-03-04 12:19:51.850 NSString[1186:65935] o
2016-03-04 12:19:51.850 NSString[1186:65935] v
2016-03-04 12:19:51.850 NSString[1186:65935] e
2016-03-04 12:19:51.850 NSString[1186:65935]  
2016-03-04 12:19:51.850 NSString[1186:65935] <
2016-03-04 12:19:51.850 NSString[1186:65935] è
2016-03-04 12:19:51.850 NSString[1186:65935] <
2016-03-04 12:19:51.851 NSString[1186:65935] í

如果你想要處理 UTF-8 或者 Unicode 碼位,你需要將原始字符串轉(zhuǎn)化成 UTF-8 或者 UTF-32 表示然后再對(duì)結(jié)果進(jìn)行操作。這種處理方式更多是將字符串視為數(shù)據(jù)對(duì)象,而不是視為字符串本身,所以在轉(zhuǎn)換時(shí)并不是很方便。 現(xiàn)在我們想通過(guò)字符簇的方式遍歷:

NSString *test = @"I Love ????";
NSRange range;
for (int i = 0;i<test.length;i++) {
        range = [test rangeOfComposedCharacterSequenceAtIndex:i];
        NSString* string = [test substringWithRange:range];
        NSLog(@"%@",string);
 }

輸出結(jié)果如下:

2016-03-04 12:31:04.061 NSString[1244:74694] I
2016-03-04 12:31:04.062 NSString[1244:74694]  
2016-03-04 12:31:04.062 NSString[1244:74694] L
2016-03-04 12:31:04.062 NSString[1244:74694] o
2016-03-04 12:31:04.062 NSString[1244:74694] v
2016-03-04 12:31:04.062 NSString[1244:74694] e
2016-03-04 12:31:04.062 NSString[1244:74694]  
2016-03-04 12:31:04.062 NSString[1244:74694] ????
2016-03-04 12:31:04.062 NSString[1244:74694] ????
2016-03-04 12:31:04.063 NSString[1244:74694] ????
2016-03-04 12:31:04.063 NSString[1244:74694] ????

盡管現(xiàn)在成功輸出了國(guó)旗,但我們可以發(fā)現(xiàn)它輸出了四個(gè),這是由于字符簇在UTF-16中造成的,仍然很不方便。
如果你要對(duì)字符簇進(jìn)行操作,還需要使用rangeOfComposedCharacterSequencesForRange:
方法找到它們和其他字符的分界位置,這是一項(xiàng)非??菰锏娜蝿?wù)。

Swift 的String類(lèi)型則采用了另外一種方法。在這里面沒(méi)有標(biāo)準(zhǔn)化的表示,而是為字符串的不同表示方式提供了視圖(view)。這樣無(wú)論處理哪種表示方式,你都能夠靈活自如地操作。

五、簡(jiǎn)述 Swift 中的 String API

在舊版本中的 Swift 中,String類(lèi)遵循了CollectionType接口,將自己看做是Character元素的集合。在 Swift 2 中,這種表示已經(jīng)不復(fù)存在,String類(lèi)會(huì)根據(jù)使用的不同情況,展現(xiàn)出不同的表現(xiàn)方式。
這種表示方式還不是很完善,String仍然有點(diǎn)傾向于Character集合的表示方式,它依舊提供了有點(diǎn)類(lèi)似集合處理的接口:

public typealias Index = String.CharacterView.Index
public var startIndex: Index { get }
public var endIndex: Index { get }
public subscript (i: Index) -> Character { get }

你可以通過(guò)String的索引獲得單獨(dú)的Character。注意,你并不能通過(guò)標(biāo)準(zhǔn)的for in語(yǔ)法遍歷整個(gè)字符串。
在 Swift 看來(lái),一個(gè)「字符」究竟是什么?正如我們所見(jiàn),有太多的可能性。Swift 中 String API 的實(shí)現(xiàn)基礎(chǔ)是將一個(gè)字素簇看作一個(gè)「字符」。這看上去是一個(gè)非常不錯(cuò)的選擇,因?yàn)檎缥覀兯?jiàn),這種方式符合人類(lèi)在字符串中對(duì)于一個(gè)「字符」的定義。
不同的視圖在String類(lèi)中作為屬性展現(xiàn)。例如,characters屬性:

public var characters: String.CharacterView { get }

CharacterView是Character的一個(gè)集合:

extension String.CharacterView : CollectionType { 
public struct Index ... 
public var startIndex: String.CharacterView.Index { get } 
public var endIndex: String.CharacterView.Index { get }
public subscript (i: String.CharacterView.Index) -> Character { get }
}

這看上去有點(diǎn)像String接口本身,除了它遵循CollectionType
協(xié)議并且擁有所有CollectionType提供的方法外,它實(shí)現(xiàn)了劃分(slice)、遍歷(iterate)、映射(map)或者計(jì)數(shù)(count)方法。

let string:String = "I Love ????"
let b = string.characters.endIndex
print("\(b)") //結(jié)果仍然是11,說(shuō)明是UTF-16

所以盡管下面的方法是不被允許的:
for x in "abc" {}
但是這是行得通的:
for x in "abc".characters {}
你可以使用構(gòu)造函數(shù)從CharacterView中獲得一個(gè)字符串:
public init(_ characters: String.CharacterView)

super.viewDidLoad()
let string:String = "I Love ????"
let string2 = String.init(string.characters)  //string2為"I Love ????"

你甚至可以從隨機(jī)序列中獲取Character作為一個(gè)字符串:

public init<S : SequenceType where S.Generator.Element == Character>(_ characters: S)
// 譯者注:現(xiàn)在是 public init(_ characters: String.CharacterView)

繼續(xù)我們的旅程,下一個(gè)是 UTF-32 字符視圖。Swift 把 UTF-32 碼元叫做「Unicode 標(biāo)量(unicode scalars)」(譯者注:參看 Unicode scalar values),因?yàn)?UTF-32 碼元與 Unicode 碼位是等同的。這個(gè)(簡(jiǎn)化的)接口看上去是這樣的:

public var unicodeScalars: String.UnicodeScalarView
public struct UnicodeScalarView : CollectionType, _Reflectable,CustomStringConvertible, CustomDebugStringConvertible { 
public struct Index ... 
public var startIndex: String.UnicodeScalarView.Index { get } 
public var endIndex: String.UnicodeScalarView.Index { get } 
public subscript (position: String.UnicodeScalarView.Index) -> UnicodeScalar { get }
}

類(lèi)似于CharacterView,在UnicodeScalarView內(nèi)部也有個(gè)String的構(gòu)造函數(shù):public init(_ unicodeScalars: String.UnicodeScalarView)
不幸的是,UnicodeScalar序列沒(méi)有實(shí)例化方法,所以在操作時(shí)需要做一點(diǎn)額外工作,例如,需要將這些字符轉(zhuǎn)換成數(shù)組,然后再將數(shù)組轉(zhuǎn)化成字符串。同時(shí),在UnicodeScalarView中也沒(méi)有接受UnicodeScalar
序列作為參數(shù)的實(shí)例化方法。然而,Swift 提供了一個(gè)在尾部添加元素的函數(shù),所以你可以通過(guò)下面三步建立一個(gè)String。

var unicodeScalarsView =String.UnicodeScalarView()
unicodeScalarsView.appendContentsOf(unicodeScalarsArray)
let unicodeScalarsString = String(unicodeScalarsView)

接下來(lái)是 UTF-16 字符視圖,看上去和其他的也很類(lèi)似:

public var utf16: String.UTF16View { get }
public struct UTF16View : CollectionType { 
public struct Index ... 
public var startIndex: String.UTF16View.Index { get } 
public var endIndex: String.UTF16View.Index { get } 
public subscript (i: String.UTF16View.Index) -> CodeUnit { get }
}

在這個(gè)視圖中,String
的實(shí)例化方法又有細(xì)微的差別:

public init?(_ utf16: String.UTF16View)

與其他的方法不同,這是一個(gè)可能會(huì)構(gòu)造失敗的構(gòu)造方法(譯者注:注意init?
)。任何Character或者UnicodeScalar的序列都是一個(gè)合法的String
,但是對(duì)于以 UTF-16 作為碼元的序列,可能無(wú)法將其轉(zhuǎn)化成一個(gè)合法的字符串。當(dāng)內(nèi)容非法時(shí),構(gòu)造方法將返回nil。
將任意一個(gè) UTF-16 碼元序列轉(zhuǎn)換成一個(gè)String類(lèi)型的字符串非常困難。UTF16View沒(méi)有公共的構(gòu)造方法,并且只有很少幾個(gè)轉(zhuǎn)換方法。這個(gè)問(wèn)題的解決方法就是使用全局transcode函數(shù),它已經(jīng)遵循UnicodeCodecType協(xié)議。UTF8、UTF16和UTF32這三個(gè)類(lèi)中分別實(shí)現(xiàn)了這個(gè)協(xié)議,通過(guò)transcode函數(shù)可以實(shí)現(xiàn)三者的互相轉(zhuǎn)化,雖然很不優(yōu)雅。對(duì)于輸入,函數(shù)接受一個(gè)GeneratorType類(lèi)型的參數(shù),中間通過(guò)一個(gè)用于產(chǎn)生輸出結(jié)果每一位的函數(shù)進(jìn)行轉(zhuǎn)化。這可將一個(gè)UTF16字符串一點(diǎn)一點(diǎn)地轉(zhuǎn)化成UTF32類(lèi)型字符串,接著再將每個(gè)UTF-32
碼元轉(zhuǎn)化成對(duì)應(yīng)的UnicodeScalar,拼接到String中:

var utf16String = ""
transcode(UTF16.self, UTF32.self, utf16Array.generate(), { utf16String.append(UnicodeScalar($0)) }, stopOnError: true)
// 譯者注:transcode 方法的幾個(gè)參數(shù):
// 1. inputEncoding: InputEncoding.Type
// 2. _ outputEncoding: OutputEncoding.Type
// 3. _ input: Input
// 4. _ output: (OutputEncoding.CodeUnit) -> ()
// 5. stopOnError: Bool
// 這里缺少 utf16Array,可以嘗試在第二行代碼前加入
// let utf16Array = Array(String(count: 9999, repeatedValue: Character("X")).utf16)// 來(lái)測(cè)試結(jié)果

最后我們來(lái)看一下 UTF-8 字符視圖。實(shí)現(xiàn)方式和我們之前介紹的一樣:

public var utf8: String.UTF8View { get }
public struct UTF8View : CollectionType { /// A position in a `String.UTF8View`.
public struct Index ... 
public var startIndex: String.UTF8View.Index { get } 
public var endIndex: String.UTF8View.Index { get } public subscript (position: String.UTF8View.Index) -> CodeUnit { get }}

另外在定義中也有一個(gè)構(gòu)造函數(shù)。和UTF16View
一樣,這也是一個(gè)可能失敗的構(gòu)造方法,因?yàn)橛?UTF-8 碼元組成的序列也有可能是不合法的。
public init?(_ utf8: String.UTF8View)

和前者類(lèi)似,這兒也沒(méi)有一種簡(jiǎn)便的方法將任意一個(gè) UTF-8 碼元組成的序列轉(zhuǎn)換成String
類(lèi)型。仍然可以使用 transcode 方法:

extension String {
    init?<Seq: SequenceType where Seq.Generator.Element == UInt16>(utf16: Seq) {
        self.init()
        
        guard transcode(UTF16.self,
            UTF32.self,
            utf16.generate(),
            { self.append(UnicodeScalar($0)) },
            stopOnError: true)
            == false else { return nil }
    }
    
    init?<Seq: SequenceType where Seq.Generator.Element == UInt8>(utf8: Seq) {
        self.init() 
        guard transcode(UTF8.self,
            UTF32.self,
            utf8.generate(),
            { self.append(UnicodeScalar($0)) },
            stopOnError: true)
            == false else { return nil }
    }
}

六、索引

上面介紹的不同視圖都可用于索引 (Indexes)集合,但是它們并是數(shù)組。索引類(lèi)型是一種非常詭異的自定義結(jié)構(gòu)體(struct)。
這意味著你不能通過(guò)數(shù)字來(lái)讀取不同視圖中的內(nèi)容:

// all errors
string[2]
string.characters[2]
string.unicodeScalars[2]
string.utf16[2]
string.utf8[2]

不過(guò)你可以使用集合類(lèi)型的startIndex或者是endIndex屬性,并且使用successor()或者advancedBy()方法來(lái)移動(dòng)到合適的位置:

// these work
string[string.startIndex.advancedBy(2)]
string.characters[string.characters.startIndex.advancedBy(2)]
string.unicodeScalars[string.unicodeScalars.startIndex.advancedBy(2)]
string.utf16[string.utf16.startIndex.advancedBy(2)]
string.utf8[string.utf8.startIndex.advancedBy(2)]

這并不是件有趣的事,我們想知道到底發(fā)生了什么?
還記得這些以標(biāo)準(zhǔn)化表示保存在字符串對(duì)象的視圖嗎?當(dāng)你使用了一個(gè)不符合標(biāo)準(zhǔn)化表示形式的視圖時(shí),存儲(chǔ)的數(shù)據(jù)并不能自動(dòng)轉(zhuǎn)化成你想要的形式。
回想一下上面所提到的,不同的編碼方式有不同的大小和長(zhǎng)度。這也意味著無(wú)法簡(jiǎn)單地判斷字符在不同視圖中對(duì)應(yīng)的位置,因?yàn)樗成涞降奈恢檬歉鶕?jù)保存的數(shù)據(jù)不同而不同的??紤]下面這個(gè)字符串:A?工??

這個(gè)String類(lèi)型的字符串在 UTF-32 編碼下的標(biāo)準(zhǔn)化表示是幾個(gè) 32 位整型元素的集合:

0x00041 0x0018e 0x05de5 0x1f11e

我們?cè)僬驹?UTF-8 編碼的視角上來(lái)看這些數(shù)據(jù)。理論上說(shuō),這些數(shù)據(jù)就是一組 8 位整型元素的序列:

0x41 0xc6 0x8e 0xe5 0xb7 0xa5 0xf0 0x9f 0x84 0x9e

下面是兩者的映射關(guān)系:

| 0x00041 | 0x0018e  | 0x05de5        | 0x1f11e             |
|         |          |                |                     |
| 0x41    | 0xc6 0x8e| 0xe5 0xb7 0xa5 | 0xf0 0x9f 0x84 0x9e |

如果需要獲取在 UTF-8 視圖下索引為 6 的值,那么必須去從 UTF-32 的序列中從頭開(kāi)始去掃描,然后獲取所在位置所對(duì)應(yīng)的值。
顯然,這是可以做到的。Swift 提供了這種底層方法,但是長(zhǎng)得并不好看:string.utf8[string.utf8.startIndex.advancedBy(6)]
。為什么不能簡(jiǎn)化這種表示,直接用一個(gè)整數(shù)來(lái)訪問(wèn)索引呢?實(shí)際上 Swift 為了加強(qiáng)這種表示犧牲了簡(jiǎn)潔性。在一個(gè)UTF8View能提供subscript(Int)(譯者注:即下標(biāo)索引)方法的世界里,我們希望下面兩段代碼是等價(jià)的:

for c in string.utf8 { ...}
for i in 0..<string.utf8.count { let c = string.utf8[i] ...}

這看上去很相似,但是第二個(gè)會(huì)意外地更慢一些。第一個(gè)循環(huán)是一個(gè)線性時(shí)間的掃描,然而第二個(gè)循環(huán)需要對(duì)每次迭代做一次線性掃描,即需要用二次方項(xiàng)的時(shí)間來(lái)做迭代遍歷。對(duì)于一個(gè)長(zhǎng)度為一百萬(wàn)的字符串,第一個(gè)循環(huán)只需要 0.1 秒,而第二個(gè)循環(huán)需要 3 個(gè)小時(shí)(在我的 2013 年 MacBook Pro 上進(jìn)行的測(cè)試)。
我們?cè)賮?lái)看另外一個(gè)例子,從字符串中獲得最后一個(gè)字符:

let lastCharacter = string.characters[string.characters.endIndex.predecessor()]
let lastCharacter = string.characters[string.characters.count - 1]

第一個(gè)版本會(huì)更快一些。因?yàn)樗苯訌淖址淖詈箝_(kāi)始,從最后一個(gè)Character
開(kāi)始的地方從后往前搜索,然后獲取字符。第二個(gè)版本會(huì)掃描整個(gè)字符串……兩次!它首先得掃描整個(gè)字符串來(lái)獲取有多少個(gè)Character
,接著再一次掃描特定序號(hào)的字符是什么。
類(lèi)似這樣的 API 在 Swift 中只是有點(diǎn)不同、有點(diǎn)難寫(xiě)。這些不同之處讓程序員們知道了視圖并不是數(shù)組,它們也沒(méi)有數(shù)組的行為。當(dāng)我們使用下標(biāo)索引時(shí),我們事實(shí)上假設(shè)了這種操作是一種效率很高的行為。如果String的視圖提供了這種索引,那其實(shí)和我們的主觀假設(shè)相反,只能寫(xiě)出效率很低的代碼。

七、使用 String 類(lèi)來(lái)寫(xiě)代碼吧

在應(yīng)用層面上使用 String 類(lèi)寫(xiě)代碼意味著什么呢?

你可以使用頂層 API。舉個(gè)例子,如果你需要判斷一個(gè)字符串是否是以某個(gè)字符開(kāi)頭的,那不需要對(duì)字符串索引然后獲取第一個(gè)字符并做比較。直接使用 hasPrefix 方法,它已經(jīng)為你準(zhǔn)備好了一切。不要害怕導(dǎo)入 Foundation 庫(kù)和使用 NSString 中的方法。當(dāng)你想移除 String 開(kāi)頭和結(jié)尾多余的空格時(shí),不必手動(dòng)遍歷獲取這些字符,可以直接使用 stringByTrimmingCharactersInSet 方法。

如果你需要做一些字符層面的事情,那么就要想象一下,對(duì)于特定情況一個(gè)「字符」意味著什么。通常,正確答案是指一個(gè)字素簇,這在 Swift 中表示成 Character 類(lèi)型,展現(xiàn)在 characters 視圖中。

無(wú)論你需要對(duì)文本做些什么事情,都要思考一下對(duì)文本從頭到尾線性掃描的事情。諸如計(jì)算有多少個(gè)字符、查找中間的字符這類(lèi)操作會(huì)消耗線性的時(shí)間,所以你最好整理一下代碼,更加干凈利落地做這些線性時(shí)間掃描的操作。對(duì)于特定的視圖,取得開(kāi)始和結(jié)束的下標(biāo)索引,在必要的時(shí)候使用 advancedBy() 或者其他類(lèi)似的方法來(lái)移動(dòng)索引的位置。

八、總結(jié)

Swift 中的String類(lèi)型采取了一種與眾不同的方法來(lái)處理字符串。其他很多語(yǔ)言會(huì)選擇一種標(biāo)準(zhǔn)化表示法,然后將轉(zhuǎn)換等操作留給程序員自己去處理。通常它們?cè)凇傅降资裁床攀亲址俊惯@種重要的問(wèn)題上做出了妥協(xié),它們?cè)谔幚碜址臅r(shí)候,直接在編碼中加入一些「語(yǔ)法糖」來(lái)讓代碼更加易寫(xiě),然而這本質(zhì)上就會(huì)導(dǎo)致各種困難的發(fā)生。Swift 語(yǔ)法可能沒(méi)那么「甜」,相反則是在告訴你實(shí)際上會(huì)發(fā)生什么。對(duì)于程序員來(lái)說(shuō),這會(huì)比較困難,但其實(shí)也就只有這些困難。
String的 API 中也有一些坑,但是我們可以使用一些其他的方法來(lái)讓操作稍微簡(jiǎn)單一些。特別地,從 UTF-8 或 UTF-16 轉(zhuǎn)換成一個(gè)String
類(lèi)型的數(shù)據(jù)是一件困難而又煩人的事。如果我們有一些能夠?qū)⑷我庖淮a元序列轉(zhuǎn)換成字符串的UTF8View和UTF16View構(gòu)造方法,以及另外一些直接建立在這些視圖上的轉(zhuǎn)換方法,那么 Swift 中的String類(lèi)型將變得更加友好。
今天就到這里了。希望下次還能給大家?guī)?lái)更多驚喜。周五問(wèn)答的主題是根據(jù)大家的想法產(chǎn)生的,所以記得給我們寫(xiě)信來(lái)提出你想要聽(tīng)的話題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容