Swift中的String API

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

一、什么是字符串?

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

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

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

二、問題

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

aé∞??

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

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

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

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

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

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

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

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

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

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

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

á∞??

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

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

讓我們再把這個字符串當(dāng)做 UTF-16 編碼來看。在 UTF-16 編碼下,這個字符串看上去長這個樣子:

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

我們嘗試著移除第二個「字符」:

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

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

0061 0065 0301 221e dd1e

和不合法的 UTF-8 編碼是類似的問題,這個序列也不再是一個合法的 UTF-16 字符串。序列

d834 dd1e

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

aé∞?

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

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

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

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

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

三、字素簇(Grapheme Clusters)

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

+ 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

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

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

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

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

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

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

那么當(dāng)你在實(shí)現(xiàn)字符串時,如何來平衡性能、內(nèi)存使用和簡潔代碼三者呢?
通常的回答是選擇一種標(biāo)準(zhǔn)化表示(canonical representation),之后在需要其他表示方法時進(jìn)行轉(zhuǎn)換。例如,NSString使用 UTF-16 作為其標(biāo)準(zhǔn)化表示法。整個 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 表示然后再對結(jié)果進(jìn)行操作。這種處理方式更多是將字符串視為數(shù)據(jù)對象,而不是視為字符串本身,所以在轉(zhuǎn)換時并不是很方便。 現(xiàn)在我們想通過字符簇的方式遍歷:

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)在成功輸出了國旗,但我們可以發(fā)現(xiàn)它輸出了四個,這是由于字符簇在UTF-16中造成的,仍然很不方便。
如果你要對字符簇進(jìn)行操作,還需要使用rangeOfComposedCharacterSequencesForRange:
方法找到它們和其他字符的分界位置,這是一項(xiàng)非常枯燥的任務(wù)。

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

五、簡述 Swift 中的 String API

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

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

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

public var characters: String.CharacterView { get }

CharacterView是Character的一個集合:

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)或者計數(shù)(count)方法。

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

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

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

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

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

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

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 }
}

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

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

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

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 }
}

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

public init?(_ utf16: String.UTF16View)

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

var utf16String = ""
transcode(UTF16.self, UTF32.self, utf16Array.generate(), { utf16String.append(UnicodeScalar($0)) }, stopOnError: true)
// 譯者注:transcode 方法的幾個參數(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)// 來測試結(jié)果

最后我們來看一下 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òu)造函數(shù)。和UTF16View
一樣,這也是一個可能失敗的構(gòu)造方法,因?yàn)橛?UTF-8 碼元組成的序列也有可能是不合法的。
public init?(_ utf8: String.UTF8View)

和前者類似,這兒也沒有一種簡便的方法將任意一個 UTF-8 碼元組成的序列轉(zhuǎn)換成String
類型。仍然可以使用 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ù)組。索引類型是一種非常詭異的自定義結(jié)構(gòu)體(struct)。
這意味著你不能通過數(shù)字來讀取不同視圖中的內(nèi)容:

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

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

// 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)化表示保存在字符串對象的視圖嗎?當(dāng)你使用了一個不符合標(biāo)準(zhǔn)化表示形式的視圖時,存儲的數(shù)據(jù)并不能自動轉(zhuǎn)化成你想要的形式。
回想一下上面所提到的,不同的編碼方式有不同的大小和長度。這也意味著無法簡單地判斷字符在不同視圖中對應(yīng)的位置,因?yàn)樗成涞降奈恢檬歉鶕?jù)保存的數(shù)據(jù)不同而不同的。考慮下面這個字符串:A?工??

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

0x00041 0x0018e 0x05de5 0x1f11e

我們再站在 UTF-8 編碼的視角上來看這些數(shù)據(jù)。理論上說,這些數(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 的序列中從頭開始去掃描,然后獲取所在位置所對應(yīng)的值。
顯然,這是可以做到的。Swift 提供了這種底層方法,但是長得并不好看:string.utf8[string.utf8.startIndex.advancedBy(6)]
。為什么不能簡化這種表示,直接用一個整數(shù)來訪問索引呢?實(shí)際上 Swift 為了加強(qiáng)這種表示犧牲了簡潔性。在一個UTF8View能提供subscript(Int)(譯者注:即下標(biāo)索引)方法的世界里,我們希望下面兩段代碼是等價的:

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

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

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

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

七、使用 String 類來寫代碼吧

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

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

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

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

八、總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,241評論 2 375

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