我發(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)生的,所以記得給我們寫信來提出你想要聽的話題。