作者:Mike Ash,原文鏈接,原文日期:2015-11-06
譯者:Cee;校對(duì):numbbbbb;定稿:numbbbbb
譯者注:可以結(jié)合 WWDC 2015 Session 227 - What's New in Internationalization 一起學(xué)習(xí)
歡迎來到本期因修改了很多次稿而推遲發(fā)布的周五問答。我發(fā)現(xiàn)很多人在使用 Swift 時(shí),都會(huì)抱怨 String
API 很難用。它很難學(xué)習(xí)并且設(shè)計(jì)得晦澀難懂,大多數(shù)人希望它能采用其他語言的字符串(String) API 設(shè)計(jì)風(fēng)格。今天我就要來講一下為什么 Swift 中的 String
API 會(huì)被設(shè)計(jì)成現(xiàn)在這樣(最起碼要解釋清楚我的看法),以及為什么我最終會(huì)認(rèn)為,就其基礎(chǔ)設(shè)計(jì)而言 Swift 中的 String
API 是字符串 API 中設(shè)計(jì)得最好的。
什么是字符串?
在我們討論這點(diǎn)之前,首先需要建立一個(gè)基本的概念。我們總是把字符串想得很膚淺,很少有人能夠深入思考它的本質(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)有哪個(gè)語言或者標(biāo)準(zhǔn)庫做到了這點(diǎn)。)
那么在底層,這些常見的「文本」概念又是怎么被表示的呢?唔,得看情況。有很多不同的解決方法。
在很多語言中,字符串是用于存放字節(jié)(bytes)的數(shù)組(array)。程序所要做的就是為這些字節(jié)賦值。這種字符串的表示方法在 C++ 中是 std::string
類型,Python 2、Go 和其他語言也是這樣。
C 語言對(duì)于字符串的表示就比較古怪和特殊。在 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
。這可能是一個(gè)歷史遺留問題。Unicode 在 1991 年被提出時(shí)(譯者注:1991 年 10 月發(fā)布 Unicode 1.0.0),當(dāng)時(shí)的系統(tǒng)都是 16 位。很多流行的編程語言在那個(gè)時(shí)代被設(shè)計(jì)出來,并且將 Unicode 作為字符串的構(gòu)成基礎(chǔ)。在 1996 年,Unicode 在 16 位系統(tǒng)上經(jīng)歷了爆發(fā)性的增長(譯者注:1996 年 7 月發(fā)布了 Unicode 2.0,字庫從 7161 個(gè)字元變成了 38950 個(gè)),這些語言再要改變字符串的編碼方式已為時(shí)已晚。這時(shí),由于 UTF-16 的編碼方式能夠?qū)⒏蟮臄?shù)字編碼為一組 16 位碼元的集合,因此將字符串視為 16 位碼元序列的基本想法就這樣延續(xù)了下來。
這種想法的一個(gè)變體就是將字符串定義成 UTF-8 碼元序列,其中組成的碼元是 8 位的。總體上來說和 UTF-16 的表示方法很接近,但是對(duì)于 ASCII 字符串來說,能夠有更加緊湊的表示空間,而且避免了在傳遞字符串進(jìn)入函數(shù)時(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
類型。
簡短概括一下,一個(gè)字符串通常情況下會(huì)被當(dāng)做某些特定字符(character)的序列,其中字符通常是一個(gè)字節(jié),或者是一個(gè) UTF-16 碼元,又或者是一個(gè) Unicode 碼位。
問題
將字符串表示成一段連續(xù)「字符」的序列的確很方便。你可以把字符串看作是數(shù)組(array)(通常情況下就是個(gè)數(shù)組),這樣就很容易獲得字符串的子串、從字符串頭部或者尾部取出部分元素、刪除字符串的某部分、獲取字符總數(shù),等等。
問題是我們身邊遍布著 Unicode,而 Unicode 會(huì)讓事情變得很復(fù)雜。簡單看一個(gè)字符串的例子,看一下它是怎么工作的:
aé∞??
每一個(gè) Unicode 碼位都有一串?dāng)?shù)字(寫作 U+nnnn)和一個(gè)供我們看得懂的命名(某種原因使用全大寫的英文字母表示),這樣我們更容易討論單個(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 三種不同的字符編碼方式來講解。
首先將這個(gè)「字符」看作是一個(gè) UTF-8 字符單元。這個(gè)字符串在 UTF-8 下看上去長這樣:
61 65 cc 81 e2 88 9e f0 9d 84 9e
-- -- ----- -------- -----------
a e ′ ∞ ??
我們來移除第 3 個(gè)「字符」,即第三個(gè)字節(jié)(cc)。結(jié)果是:
61 65 81 e2 88 9e f0 9d 84 9e
這個(gè)字符串已經(jīng)不再是個(gè)合法的 UTF-8 字符串。UTF-8 的字符編碼有三類。對(duì)于那些 0xxxxxxx
表示的,即由 0 開頭的,會(huì)被表示為 ASCII 字符,單獨(dú)歸為第一類。那些看上去形如 11xxxxxx
的,表示一個(gè)多位序列,長度由第一個(gè) 0 的位置決定。第三類表示成 10xxxxxx
,說明一個(gè)多位序列的剩余部分。cc
(譯者注:即 11001100
,劃分在第二類。其中第一個(gè) 0 出現(xiàn)在從 0 開始計(jì)數(shù)的第 2 位,故整個(gè)多位序列由兩個(gè)字節(jié)組成)表示了一個(gè)多位序列的開始,長度是兩個(gè)字節(jié),81
(譯者注:即 10000001
,劃分在第三類)表示了這個(gè)多位序列的尾部。如果移除了 cc
,那么剩下的 81
將會(huì)被留在字符串中。所有 UTF-8 校驗(yàn)器都會(huì)拒絕識(shí)別這個(gè)字符串(譯者注:因?yàn)?81
并不是合法的 UTF-8 頭部字符,只有第一類和第二類的字符是合法的)。如果我們移除了從第三位之后的任意一個(gè)字符,這個(gè)問題依舊會(huì)發(fā)生。
那么如果是第二位呢?如果我們移除了這個(gè)字符,我們會(huì)得到:
61 cc 81 e2 88 9e f0 9d 84 9e
-- ----- -------- -----------
a ′ ∞ ??
看上去這依然是個(gè)合法的 UTF-8 字符串,但是結(jié)果并不是我們所期待的那樣:
á∞??
對(duì)于人類來說,在這個(gè)字符串中的「第二個(gè)字符」應(yīng)該是「é」。但是第二位上的字符僅僅是不帶語調(diào)標(biāo)記的「e」。這個(gè)語調(diào)標(biāo)記被看作是一個(gè)「連接字符(combining character)」,被單獨(dú)添加到前面的字符上。移除第二個(gè)字符僅僅是移去了「e」,導(dǎo)致這個(gè)語調(diào)標(biāo)記連接到了「a」字符上。
那么如果移去首個(gè)字符呢?最終結(jié)果是我們所想要的那樣:
65 cc 81 e2 88 9e f0 9d 84 9e
-- ----- -------- -----------
e ′ ∞ ??
讓我們再把這個(gè)字符串當(dāng)做 UTF-16 編碼來看。在 UTF-16 編碼下,這個(gè)字符串看上去長這個(gè)樣子:
0061 0065 0301 221e d834 dd1e
---- ---- ---- ---- ---------
a e ′ ∞ ??
我們嘗試著移除第二個(gè)「字符」:
0061 0301 221e d834 dd1e
---- ---- ---- ---------
a ′ ∞ ??
和上面在 UTF-8 中出現(xiàn)的問題一樣,刪除了「e」,但是沒有刪除語調(diào)標(biāo)記,導(dǎo)致這個(gè)標(biāo)記附在了「a」上面。
那么如果刪除第五個(gè)字符呢?我們得到了如下的序列:
0061 0065 0301 221e dd1e
和不合法的 UTF-8 編碼是類似的問題,這個(gè)序列也不再是一個(gè)合法的 UTF-16 字符串。序列 d834 dd1e
形成了一組代理對(duì)(surrogate pair),指兩個(gè) 16 位的單元用于表示一個(gè)超過 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è)非法的字符串。但是連接語調(diào)標(biāo)記的問題依然存在。移除第二個(gè)字符將會(huì)是這樣:
00061 00301 0221E 1D11E
----- ----- ----- -----
a ′ ∞ ??
即使使用這種表示方法,我們也無法確保結(jié)果的正確。
這些通常不是我們能夠簡單想到的問題。英語是鮮有的幾種僅使用 ASCII 字符就能表示的語言。你肯定不想把求職時(shí)的簡歷(Résumé)改成「Resume」吧!一旦超出 ASCII 字符集,這些荒謬的錯(cuò)誤就開始出現(xiàn)了。
字素簇(Grapheme Clusters)
Unicode 中有個(gè)概念叫做字素簇(Grapheme Clusters),本質(zhì)上就是閱讀時(shí)會(huì)被考慮成單個(gè)「字符」的最小單元。在大多數(shù)表示方法中,一個(gè)字素簇就等價(jià)于一個(gè)單獨(dú)的碼位,但是也有可能會(huì)表示成包括語調(diào)標(biāo)記的一部分內(nèi)容。如果我們將上面的例子表示成字素簇的方式,那么很顯然會(huì)是這樣的:
a é ∞ ??
移除任意一個(gè)作為字素簇的單元,留下的內(nèi)容都會(huì)被認(rèn)為是合情合理的。
注意到在這個(gè)例子中,并沒有任何的數(shù)字等值 (numeric equivalents) 存在。這是因?yàn)榕c UTF-8、UTF-16 或者普通的 Unicode 碼位不同,單個(gè)數(shù)字無法在一般情況下表示字素簇 (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è)包含瑞士國旗的字符串:
????
這個(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」而不是將地球上的所有國家的國旗作為單獨(dú)的碼位。將 C 和 H 的兩個(gè)標(biāo)識(shí)符合起來你就能得到瑞士的國旗。將 M 和 X 合起來會(huì)得到墨西哥國旗。每個(gè)國旗都是一個(gè)單獨(dú)的字符簇,但是由兩個(gè)碼位組成,即四個(gè) UTF-16 碼元或者八個(gè) UTF-8 字節(jié)。
字符串 API 實(shí)現(xiàn)方式
我們發(fā)現(xiàn)字符串有多種理解方法,也有多種表示「字符」的方式。將「字符」當(dāng)做一個(gè)字素簇可能最接近人們對(duì)于「字符」的理解,但是在代碼中操作字符串時(shí),要依據(jù)語言環(huán)境來判斷所謂「字符」的含義。當(dāng)在文本中移動(dòng)插入光標(biāo)時(shí),光標(biāo)經(jīng)過的字符就是指字素簇。當(dāng)為了保證文本滿足 140 字限制的推文時(shí),這里的字符就是 Unicode 碼位。當(dāng)字符串想要保存在限定長度是 80 個(gè)字符的數(shù)據(jù)庫表中時(shí),這里的字符就是個(gè) UTF-8 字節(jié)。
那么當(dāng)你在實(shí)現(xiàn)字符串時(shí),如何來平衡性能、內(nèi)存使用和簡潔代碼三者呢?
通常的回答是選擇一種標(biāo)準(zhǔn)化表示(canonical representation),之后在需要其他表示方法時(shí)進(jìn)行轉(zhuǎn)換。例如,NSString
使用 UTF-16 作為其標(biāo)準(zhǔn)化表示法。整個(gè) API 基于 UTF-16 建立。如果你想要處理 UTF-8 或者 Unicode 碼位,你需要將原始字符串轉(zhuǎn)化成 UTF-8 或者 UTF-32 表示然后再對(duì)結(jié)果進(jìn)行操作。這種處理方式更多是將字符串視為數(shù)據(jù)對(duì)象,而不是視為字符串本身,所以在轉(zhuǎn)換時(shí)并不是很方便。如果你要對(duì)字符簇進(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
類會(huì)根據(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
語法遍歷整個(gè)字符串。
在 Swift 看來,一個(gè)「字符」究竟是什么?正如我們所見,有太多的可能性。Swift 中 String API 的實(shí)現(xiàn)基礎(chǔ)是將一個(gè)字素簇看作一個(gè)「字符」。這看上去是一個(gè)非常不錯(cuò)的選擇,因?yàn)檎缥覀兯姡@種方式符合人類在字符串中對(duì)于一個(gè)「字符」的定義。
不同的視圖在 String
類中作為屬性展現(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)方法。所以盡管下面的方法是不被允許的:
for x in "abc" {}
但是這是行得通的:
for x in "abc".characters {}
你可以使用構(gòu)造函數(shù)從 CharacterView
中獲得一個(gè)字符串:
public init(_ characters: String.CharacterView)
你甚至可以從隨機(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è)(簡化的)接口看上去是這樣的:
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)部也有個(gè) String
的構(gòu)造函數(shù):
public init(_ unicodeScalars: String.UnicodeScalarView)
不幸的是,UnicodeScalar
序列沒有實(shí)例化方法,所以在操作時(shí)需要做一點(diǎn)額外工作,例如,需要將這些字符轉(zhuǎn)換成數(shù)組,然后再將數(shù)組轉(zhuǎn)化成字符串。同時(shí),在 UnicodeScalarView
中也沒有接受 UnicodeScalar
序列作為參數(shù)的實(shí)例化方法。然而,Swift 提供了一個(gè)在尾部添加元素的函數(shù),所以你可以通過下面三步建立一個(gè) 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 }
}
在這個(gè)視圖中,String
的實(shí)例化方法又有細(xì)微的差別:
public init?(_ utf16: String.UTF16View)
與其他的方法不同,這是一個(gè)可能會(huì)構(gòu)造失敗的構(gòu)造方法(譯者注:注意 init?
)。任何 Character
或者 UnicodeScalar
的序列都是一個(gè)合法的 String
,但是對(duì)于以 UTF-16 作為碼元的序列,可能無法將其轉(zhuǎn)化成一個(gè)合法的字符串。當(dāng)內(nèi)容非法時(shí),構(gòu)造方法將返回 nil
。
將任意一個(gè) UTF-16 碼元序列轉(zhuǎn)換成一個(gè) String
類型的字符串非常困難。UTF16View
沒有公共的構(gòu)造方法,并且只有很少幾個(gè)轉(zhuǎn)換方法。這個(gè)問題的解決方法就是使用全局 transcode
函數(shù),它已經(jīng)遵循 UnicodeCodecType
協(xié)議。UTF8
、UTF16
和 UTF32
這三個(gè)類中分別實(shí)現(xiàn)了這個(gè)協(xié)議,通過 transcode
函數(shù)可以實(shí)現(xiàn)三者的互相轉(zhuǎn)化,雖然很不優(yōu)雅。對(duì)于輸入,函數(shù)接受一個(gè) GeneratorType
類型的參數(shù),中間通過一個(gè)用于產(chǎn)生輸出結(jié)果每一位的函數(shù)進(jìn)行轉(zhuǎn)化。這可將一個(gè) UTF16
字符串一點(diǎn)一點(diǎn)地轉(zhuǎn)化成 UTF32
類型字符串,接著再將每個(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)
// 來測試結(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è)構(gòu)造函數(shù)。和 UTF16View
一樣,這也是一個(gè)可能失敗的構(gòu)造方法,因?yàn)橛?UTF-8 碼元組成的序列也有可能是不合法的。
public init?(_ utf8: String.UTF8View)
和前者類似,這兒也沒有一種簡便的方法將任意一個(gè) UTF-8 碼元組成的序列轉(zhuǎn)換成 String
類型。仍然可以使用 transcode 方法:
var utf8String = ""
transcode(UTF8.self, UTF32.self, utf8Array.generate(), { utf8String.append(UnicodeScalar($0)) }, stopOnError: true)
// 譯者注:自行補(bǔ)充 utf8Array
因?yàn)槊看握{(diào)用 transcode
方法實(shí)在是太痛苦了,我將它們用在了這一對(duì)可能會(huì)構(gòu)造失敗的構(gòu)造函數(shù)中:
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 }
}
}
通過這個(gè)構(gòu)造函數(shù),我們能將任意一個(gè) UTF-8 或 UTF-16 碼元組成的序列轉(zhuǎn)化成 String
類型的字符串:
String(utf16: utf16Array)
String(utf8: utf8Array)
索引
上面介紹的不同視圖都可用于索引 (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()
方法來移動(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)化成你想要的形式。
回想一下上面所提到的,不同的編碼方式有不同的大小和長度。這也意味著無法簡單地判斷字符在不同視圖中對(duì)應(yīng)的位置,因?yàn)樗成涞降奈恢檬歉鶕?jù)保存的數(shù)據(jù)不同而不同的。考慮下面這個(gè)字符串:
A?工??
這個(gè) String
類型的字符串在 UTF-32 編碼下的標(biāo)準(zhǔn)化表示是幾個(gè) 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 的序列中從頭開始去掃描,然后獲取所在位置所對(duì)應(yīng)的值。
顯然,這是可以做到的。Swift 提供了這種底層方法,但是長得并不好看:string.utf8[string.utf8.startIndex.advancedBy(6)]
。為什么不能簡化這種表示,直接用一個(gè)整數(shù)來訪問索引呢?實(shí)際上 Swift 為了加強(qiáng)這種表示犧牲了簡潔性。在一個(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í)間來做迭代遍歷。對(duì)于一個(gè)長度為一百萬的字符串,第一個(gè)循環(huán)只需要 0.1 秒,而第二個(gè)循環(huán)需要 3 個(gè)小時(shí)(在我的 2013 年 MacBook Pro 上進(jìn)行的測試)。
我們再來看另外一個(gè)例子,從字符串中獲得最后一個(gè)字符:
let lastCharacter = string.characters[string.characters.endIndex.predecessor()]
let lastCharacter = string.characters[string.characters.count - 1]
第一個(gè)版本會(huì)更快一些。因?yàn)樗苯訌淖址淖詈箝_始,從最后一個(gè) Character
開始的地方從后往前搜索,然后獲取字符。第二個(gè)版本會(huì)掃描整個(gè)字符串……兩次!它首先得掃描整個(gè)字符串來獲取有多少個(gè) Character
,接著再一次掃描特定序號(hào)的字符是什么。
類似這樣的 API 在 Swift 中只是有點(diǎn)不同、有點(diǎn)難寫。這些不同之處讓程序員們知道了視圖并不是數(shù)組,它們也沒有數(shù)組的行為。當(dāng)我們使用下標(biāo)索引時(shí),我們事實(shí)上假設(shè)了這種操作是一種效率很高的行為。如果 String
的視圖提供了這種索引,那其實(shí)和我們的主觀假設(shè)相反,只能寫出效率很低的代碼。
使用 String
類來寫代碼吧
在應(yīng)用層面上使用 String
類寫代碼意味著什么呢?
你可以使用頂層 API。舉個(gè)例子,如果你需要判斷一個(gè)字符串是否是以某個(gè)字符開頭的,那不需要對(duì)字符串索引然后獲取第一個(gè)字符并做比較。直接使用 hasPrefix
方法,它已經(jīng)為你準(zhǔn)備好了一切。不要害怕導(dǎo)入 Foundation 庫和使用 NSString
中的方法。當(dāng)你想移除 String
開頭和結(jié)尾多余的空格時(shí),不必手動(dòng)遍歷獲取這些字符,可以直接使用 stringByTrimmingCharactersInSet
方法。
如果你需要做一些字符層面的事情,那么就要想象一下,對(duì)于特定情況一個(gè)「字符」意味著什么。通常,正確答案是指一個(gè)字素簇,這在 Swift 中表示成 Character
類型,展現(xiàn)在 characters
視圖中。
無論你需要對(duì)文本做些什么事情,都要思考一下對(duì)文本從頭到尾線性掃描的事情。諸如計(jì)算有多少個(gè)字符、查找中間的字符這類操作會(huì)消耗線性的時(shí)間,所以你最好整理一下代碼,更加干凈利落地做這些線性時(shí)間掃描的操作。對(duì)于特定的視圖,取得開始和結(jié)束的下標(biāo)索引,在必要的時(shí)候使用 advancedBy()
或者其他類似的方法來移動(dòng)索引的位置。
總結(jié)
Swift 中的 String
類型采取了一種與眾不同的方法來處理字符串。其他很多語言會(huì)選擇一種標(biāo)準(zhǔn)化表示法,然后將轉(zhuǎn)換等操作留給程序員自己去處理。通常它們在「到底什么才是字符?」這種重要的問題上做出了妥協(xié),它們在處理字符串的時(shí)候,直接在編碼中加入一些「語法糖」來讓代碼更加易寫,然而這本質(zhì)上就會(huì)導(dǎo)致各種困難的發(fā)生。Swift 語法可能沒那么「甜」,相反則是在告訴你實(shí)際上會(huì)發(fā)生什么。對(duì)于程序員來說,這會(huì)比較困難,但其實(shí)也就只有這些困難。
String
的 API 中也有一些坑,但是我們可以使用一些其他的方法來讓操作稍微簡單一些。特別地,從 UTF-8 或 UTF-16 轉(zhuǎn)換成一個(gè) String
類型的數(shù)據(jù)是一件困難而又煩人的事。如果我們有一些能夠?qū)⑷我庖淮a元序列轉(zhuǎn)換成字符串的 UTF8View
和 UTF16View
構(gòu)造方法,以及另外一些直接建立在這些視圖上的轉(zhuǎn)換方法,那么 Swift 中的 String
類型將變得更加友好。
今天就到這里了。希望下次還能給大家?guī)砀囿@喜。周五問答的主題是根據(jù)大家的想法產(chǎn)生的,所以記得給我們寫信來提出你想要聽的話題。
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請?jiān)L問 http://swift.gg。