Friday Q&A 2015-11-06:為什么 Swift 中的 String API 如此難用?

作者:Mike Ash政基,原文鏈接贞铣,原文日期:2015-11-06
譯者:Cee;校對:numbbbbb沮明;定稿:numbbbbb

譯者注:可以結(jié)合 WWDC 2015 Session 227 - What's New in Internationalization 一起學習

歡迎來到本期因修改了很多次稿而推遲發(fā)布的周五問答辕坝。我發(fā)現(xiàn)很多人在使用 Swift 時,都會抱怨 String API 很難用荐健。它很難學習并且設計得晦澀難懂酱畅,大多數(shù)人希望它能采用其他語言的字符串(String) API 設計風格。今天我就要來講一下為什么 Swift 中的 String API 會被設計成現(xiàn)在這樣(最起碼要解釋清楚我的看法)江场,以及為什么我最終會認為纺酸,就其基礎設計而言 Swift 中的 String API 是字符串 API 中設計得最好的。

什么是字符串址否?

在我們討論這點之前餐蔬,首先需要建立一個基本的概念。我們總是把字符串想得很膚淺,很少有人能夠深入思考它的本質(zhì)樊诺。深思熟慮才能有助于我們理解接下來的內(nèi)容仗考。

從概念上來說,什么字符串呢词爬?從表面上看秃嗜,字符串就是一段文本。"Hello, World" 是字符串顿膨;"/Users/mikeash""Robert'); DROP TABLE Students;--" 也是字符串痪寻。

(順道講一下,我認為不應該把這些不同的文本表述概念看作是同樣的字符串類型虽惭。人類可讀的文本、文件路徑蛇尚、SQL 查詢語句芽唇,以及其他所有在概念上講并不相同的東西,在語言表示層面上都應該被表示成不同的類型取劫。我覺得這些概念上不同的字符串應當有不同的類型匆笤,這也能大幅減少 bug 數(shù)量。盡管我并沒有發(fā)現(xiàn)有哪個語言或者標準庫做到了這點谱邪。)

那么在底層炮捧,這些常見的「文本」概念又是怎么被表示的呢?唔惦银,得看情況咆课。有很多不同的解決方法。

在很多語言中扯俱,字符串是用于存放字節(jié)(bytes)的數(shù)組(array)书蚪。程序所要做的就是為這些字節(jié)賦值。這種字符串的表示方法在 C++ 中是 std::string 類型迅栅,Python 2殊校、Go 和其他語言也是這樣。

C 語言對于字符串的表示就比較古怪和特殊读存。在 C 語言中为流,字符串是指向一串非零字節(jié)序列(sequence of non-zero bytes)的指針,以零字節(jié)位表示字符串的結(jié)束让簿【床欤基本的使其實和數(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)缠黍,當時的系統(tǒng)都是 16 位。很多流行的編程語言在那個時代被設計出來药蜻,并且將 Unicode 作為字符串的構(gòu)成基礎瓷式。在 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 字符串來說酝掩,能夠有更加緊湊的表示空間,而且避免了在傳遞字符串進入函數(shù)時眷柔,由于這些函數(shù)只接受 C 語言風格類型(也就是 UTF-8 字符串)而導致的轉(zhuǎn)換期虾。

也有些語言將字符串表示為 Unicode 碼位(code point)指向的一段字符序列。Python 3 中就是這么實現(xiàn)的闯割,在很多 C 語言實現(xiàn)中也提供了內(nèi)置的 wchar_t 類型彻消。

簡短概括一下,一個字符串通常情況下會被當做某些特定字符(character)的序列宙拉,其中字符通常是一個字節(jié)宾尚,或者是一個 UTF-16 碼元,又或者是一個 Unicode 碼位谢澈。

問題

將字符串表示成一段連續(xù)「字符」的序列的確很方便煌贴。你可以把字符串看作是數(shù)組(array)(通常情況下就個數(shù)組),這樣就很容易獲得字符串的子串锥忿、從字符串頭部或者尾部取出部分元素牛郑、刪除字符串的某部分、獲取字符總數(shù)敬鬓,等等淹朋。

問題是我們身邊遍布著 Unicode笙各,而 Unicode 會讓事情變得很復雜。簡單看一個字符串的例子础芍,看一下它是怎么工作的:

aé∞??

每一個 Unicode 碼位都有一串數(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    ′      ∞          ??

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

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

這個字符串已經(jīng)不再是個合法的 UTF-8 字符串。UTF-8 的字符編碼有三類淘这。對于那些 0xxxxxxx 表示的颂翼,即由 0 開頭的,會被表示為 ASCII 字符慨灭,單獨歸為第一類。那些看上去形如 11xxxxxx 的球及,表示一個多位序列氧骤,長度由第一個 0 的位置決定。第三類表示成 10xxxxxx吃引,說明一個多位序列的剩余部分筹陵。cc(譯者注:即 11001100,劃分在第二類镊尺。其中第一個 0 出現(xiàn)在從 0 開始計數(shù)的第 2 位朦佩,故整個多位序列由兩個字節(jié)組成)表示了一個多位序列的開始,長度是兩個字節(jié)庐氮,81(譯者注:即 10000001语稠,劃分在第三類)表示了這個多位序列的尾部。如果移除了 cc弄砍,那么剩下的 81 將會被留在字符串中仙畦。所有 UTF-8 校驗器都會拒絕識別這個字符串(譯者注:因為 81 并不是合法的 UTF-8 頭部字符,只有第一類和第二類的字符是合法的)音婶。如果我們移除了從第三位之后的任意一個字符慨畸,這個問題依舊會發(fā)生。

那么如果是第二位呢衣式?如果我們移除了這個字符寸士,我們會得到:

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

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

á∞??

對于人類來說,在這個字符串中的「第二個字符」應該是「é」弱卡。但是第二位上的字符僅僅是不帶語調(diào)標記的「e」乃正。這個語調(diào)標記被看作是一個「連接字符(combining character)」,被單獨添加到前面的字符上谐宙。移除第二個字符僅僅是移去了「e」烫葬,導致這個語調(diào)標記連接到了「a」字符上。

那么如果移去首個字符呢凡蜻?最終結(jié)果是我們所想要的那樣:

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

讓我們再把這個字符串當做 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)標記,導致這個標記附在了「a」上面忠荞。

那么如果刪除第五個字符呢蒋歌?我們得到了如下的序列:

0061 0065 0301 221e dd1e

和不合法的 UTF-8 編碼是類似的問題,這個序列也不再是一個合法的 UTF-16 字符串委煤。序列 d834 dd1e 形成了一組代理對(surrogate pair)堂油,指兩個 16 位的單元用于表示一個超過 16 位的碼位(譯者注:具體計算參考 Wiki)。而讓代理對中的一部分單獨出現(xiàn)在字符串中是非法的碧绞。在 UTF-8 中通常會出錯府框,而在 UTF-16 中這種狀態(tài)會被忽略。例如讥邻,Cocoa 會將這個字符串渲染成這樣:

aé∞?

(譯者注:即平時出現(xiàn)的亂碼現(xiàn)象迫靖。)

那么如果一個字符串被表示成一串 Unicode 碼位序列呢?字符串看上去是這樣的:

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

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

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

即使使用這種表示方法发魄,我們也無法確保結(jié)果的正確盹牧。

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

字素簇(Grapheme Clusters)

Unicode 中有個概念叫做字素簇(Grapheme Clusters)踩寇,本質(zhì)上就是閱讀時會被考慮成單個「字符」的最小單元。在大多數(shù)表示方法中六水,一個字素簇就等價于一個單獨的碼位俺孙,但是也有可能會表示成包括語調(diào)標記的一部分內(nèi)容辣卒。如果我們將上面的例子表示成字素簇的方式,那么很顯然會是這樣的:

a é ∞ ??

移除任意一個作為字素簇的單元睛榄,留下的內(nèi)容都會被認為是合情合理的荣茫。

注意到在這個例子中,并沒有任何的數(shù)字等值 (numeric equivalents) 存在场靴。這是因為與 UTF-8啡莉、UTF-16 或者普通的 Unicode 碼位不同,單個數(shù)字無法在一般情況下表示字素簇 (grapheme cluster) 旨剥。所謂字素簇咧欣,指的是一個或多個碼位的序列集合。一個字素簇通常會包含一個或兩個碼位轨帜,但是某些情況下(比如 Zalgo 中)字素簇中也可能會包含大量的碼位魄咕。例如下面這個字符串:

e?????????????

這一團亂七八糟的字符串包括了 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

所有的這些碼位單元都表示一個單獨的字素簇。

下面有個有趣的例子蚌父。有這樣一個包含瑞士國旗的字符串:

????

這個標記實際上包括兩個碼位:U+1F1E8 U+1F1ED哮兰。這兩個碼位又表示什么意思呢?

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

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

字符串 API 實現(xiàn)方式

我們發(fā)現(xiàn)字符串有多種理解方法活鹰,也有多種表示「字符」的方式。將「字符」當做一個字素簇可能最接近人們對于「字符」的理解彬向,但是在代碼中操作字符串時缕棵,要依據(jù)語言環(huán)境來判斷所謂「字符」的含義虱饿。當在文本中移動插入光標時,光標經(jīng)過的字符就是指字素簇。當為了保證文本滿足 140 字限制的推文時没讲,這里的字符就是 Unicode 碼位眯娱。當字符串想要保存在限定長度是 80 個字符的數(shù)據(jù)庫表中時,這里的字符就是個 UTF-8 字節(jié)爬凑。

那么當你在實現(xiàn)字符串時徙缴,如何來平衡性能、內(nèi)存使用和簡潔代碼三者呢嘁信?

通常的回答是選擇一種標準化表示(canonical representation)娜搂,之后在需要其他表示方法時進行轉(zhuǎn)換迁霎。例如,NSString 使用 UTF-16 作為其標準化表示法百宇。整個 API 基于 UTF-16 建立考廉。如果你想要處理 UTF-8 或者 Unicode 碼位,你需要將原始字符串轉(zhuǎn)化成 UTF-8 或者 UTF-32 表示然后再對結(jié)果進行操作携御。這種處理方式更多是將字符串視為數(shù)據(jù)對象昌粤,而不是視為字符串本身,所以在轉(zhuǎn)換時并不是很方便啄刹。如果你要對字符簇進行操作涮坐,還需要使用 rangeOfComposedCharacterSequencesForRange: 方法找到它們和其他字符的分界位置,這是一項非呈木枯燥的任務袱讹。

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

簡述 Swift 中的 String API

在舊版本中的 Swift 中救巷,String 類遵循了 CollectionType 接口,將自己看做是 Character 元素的集合句柠。在 Swift 2 中浦译,這種表示已經(jīng)不復存在,String 類會根據(jù)使用的不同情況溯职,展現(xiàn)出不同的表現(xiàn)方式精盅。

這種表示方式還不是很完善,String 仍然有點傾向于 Character 集合的表示方式谜酒,它依舊提供了有點類似集合處理的接口:

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

你可以通過 String 的索引獲得單獨的 Character叹俏。注意,你并不能通過標準的 for in 語法遍歷整個字符串甚带。

在 Swift 看來,一個「字符」究竟是什么佳头?正如我們所見鹰贵,有太多的可能性。Swift 中 String API 的實現(xiàn)基礎是將一個字素簇看作一個「字符」康嘉。這看上去是一個非常不錯的選擇碉输,因為正如我們所見,這種方式符合人類在字符串中對于一個「字符」的定義亭珍。

不同的視圖在 String 類中作為屬性展現(xiàn)敷钾。例如枝哄,characters 屬性:

    public var characters: String.CharacterView { get }

CharacterViewCharacter 的一個集合:

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

這看上去有點像 String 接口本身,除了它遵循 CollectionType 協(xié)議并且擁有所有 CollectionType 提供的方法外阻荒,它實現(xiàn)了劃分(slice)挠锥、遍歷(iterate)、映射(map)或者計數(shù)(count)方法侨赡。所以盡管下面的方法是不被允許的:

    for x in "abc" {}

但是這是行得通的:

    for x in "abc".characters {}

你可以使用構(gòu)造函數(shù)從 CharacterView 中獲得一個字符串:

    public init(_ characters: String.CharacterView)

你甚至可以從隨機序列中獲取 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 標量(unicode scalars)」(譯者注:參看 Unicode scalar values)羊壹,因為 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 序列沒有實例化方法稠茂,所以在操作時需要做一點額外工作,例如情妖,需要將這些字符轉(zhuǎn)換成數(shù)組睬关,然后再將數(shù)組轉(zhuǎn)化成字符串。同時鲫售,在 UnicodeScalarView 中也沒有接受 UnicodeScalar 序列作為參數(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 的實例化方法又有細微的差別:

    public init?(_ utf16: String.UTF16View)

與其他的方法不同溺忧,這是一個可能會構(gòu)造失敗的構(gòu)造方法(譯者注:注意 init?)禽拔。任何 Character 或者 UnicodeScalar 的序列都是一個合法的 String,但是對于以 UTF-16 作為碼元的序列,可能無法將其轉(zhuǎn)化成一個合法的字符串历筝。當內(nèi)容非法時,構(gòu)造方法將返回 nil狂男。

將任意一個 UTF-16 碼元序列轉(zhuǎn)換成一個 String 類型的字符串非常困難敢靡。UTF16View 沒有公共的構(gòu)造方法,并且只有很少幾個轉(zhuǎn)換方法夜惭。這個問題的解決方法就是使用全局 transcode 函數(shù)姻灶,它已經(jīng)遵循 UnicodeCodecType 協(xié)議。UTF8诈茧、UTF16UTF32 這三個類中分別實現(xiàn)了這個協(xié)議产喉,通過 transcode 函數(shù)可以實現(xiàn)三者的互相轉(zhuǎn)化,雖然很不優(yōu)雅。對于輸入曾沈,函數(shù)接受一個 GeneratorType 類型的參數(shù)这嚣,中間通過一個用于產(chǎn)生輸出結(jié)果每一位的函數(shù)進行轉(zhuǎn)化。這可將一個 UTF16 字符串一點一點地轉(zhuǎn)化成 UTF32 類型字符串塞俱,接著再將每個 UTF-32 碼元轉(zhuǎn)化成對應的 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 字符視圖敛腌。實現(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)造方法像樊,因為由 UTF-8 碼元組成的序列也有可能是不合法的尤莺。

    public init?(_ utf8: String.UTF8View)

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

    var utf8String = ""
    transcode(UTF8.self, UTF32.self, utf8Array.generate(), { utf8String.append(UnicodeScalar($0)) }, stopOnError: true)
    // 譯者注:自行補充 utf8Array

因為每次調(diào)用 transcode 方法實在是太痛苦了颤霎,我將它們用在了這一對可能會構(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òu)造函數(shù),我們能將任意一個 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() 方法來移動到合適的位置:

    // 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ā)生了什么?

還記得這些以標準化表示保存在字符串對象的視圖嗎搁料?當你使用了一個不符合標準化表示形式的視圖時或详,存儲的數(shù)據(jù)并不能自動轉(zhuǎn)化成你想要的形式。

回想一下上面所提到的郭计,不同的編碼方式有不同的大小和長度霸琴。這也意味著無法簡單地判斷字符在不同視圖中對應的位置,因為所映射到的位置是根據(jù)保存的數(shù)據(jù)不同而不同的昭伸∥喑耍考慮下面這個字符串:

A?工??

這個 String 類型的字符串在 UTF-32 編碼下的標準化表示是幾個 32 位整型元素的集合:

0x00041 0x0018e 0x05de5 0x1f11e

我們再站在 UTF-8 編碼的視角上來看這些數(shù)據(jù)。理論上說庐杨,這些數(shù)據(jù)就是一組 8 位整型元素的序列:

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

下面是兩者的映射關系:

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

如果需要獲取在 UTF-8 視圖下索引為 6 的值选调,那么必須去從 UTF-32 的序列中從頭開始去掃描,然后獲取所在位置所對應的值灵份。

顯然仁堪,這是可以做到的。Swift 提供了這種底層方法各吨,但是長得并不好看:string.utf8[string.utf8.startIndex.advancedBy(6)]枝笨。為什么不能簡化這種表示,直接用一個整數(shù)來訪問索引呢揭蜒?實際上 Swift 為了加強這種表示犧牲了簡潔性横浑。在一個 UTF8View 能提供 subscript(Int)(譯者注:即下標索引) 方法的世界里,我們希望下面兩段代碼是等價的:

    for c in string.utf8 {
        ...
    }

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

這看上去很相似屉更,但是第二個會意外地更慢一些徙融。第一個循環(huán)是一個線性時間的掃描,然而第二個循環(huán)需要對每次迭代做一次線性掃描瑰谜,即需要用二次方項的時間來做迭代遍歷欺冀。對于一個長度為一百萬的字符串,第一個循環(huán)只需要 0.1 秒萨脑,而第二個循環(huán)需要 3 個小時(在我的 2013 年 MacBook Pro 上進行的測試)隐轩。

我們再來看另外一個例子,從字符串中獲得最后一個字符:

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

    let lastCharacter = string.characters[string.characters.count - 1]

第一個版本會更快一些渤早。因為它直接從字符串的最后開始职车,從最后一個 Character 開始的地方從后往前搜索,然后獲取字符鹊杖。第二個版本會掃描整個字符串……兩次悴灵!它首先得掃描整個字符串來獲取有多少個 Character,接著再一次掃描特定序號的字符是什么骂蓖。

類似這樣的 API 在 Swift 中只是有點不同积瞒、有點難寫。這些不同之處讓程序員們知道了視圖并不是數(shù)組登下,它們也沒有數(shù)組的行為茫孔。當我們使用下標索引時,我們事實上假設了這種操作是一種效率很高的行為庐船。如果 String 的視圖提供了這種索引银酬,那其實和我們的主觀假設相反,只能寫出效率很低的代碼筐钟。

使用 String 類來寫代碼吧

在應用層面上使用 String 類寫代碼意味著什么呢揩瞪?

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

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

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

總結(jié)

Swift 中的 String 類型采取了一種與眾不同的方法來處理字符串崖堤。其他很多語言會選擇一種標準化表示法侍咱,然后將轉(zhuǎn)換等操作留給程序員自己去處理。通常它們在「到底什么才是字符密幔?」這種重要的問題上做出了妥協(xié)楔脯,它們在處理字符串的時候,直接在編碼中加入一些「語法糖」來讓代碼更加易寫胯甩,然而這本質(zhì)上就會導致各種困難的發(fā)生昧廷。Swift 語法可能沒那么「甜」,相反則是在告訴你實際上會發(fā)生什么偎箫。對于程序員來說木柬,這會比較困難,但其實也就只有這些困難淹办。

String 的 API 中也有一些坑眉枕,但是我們可以使用一些其他的方法來讓操作稍微簡單一些。特別地怜森,從 UTF-8 或 UTF-16 轉(zhuǎn)換成一個 String 類型的數(shù)據(jù)是一件困難而又煩人的事速挑。如果我們有一些能夠?qū)⑷我庖淮a元序列轉(zhuǎn)換成字符串的 UTF8ViewUTF16View 構(gòu)造方法,以及另外一些直接建立在這些視圖上的轉(zhuǎn)換方法副硅,那么 Swift 中的 String 類型將變得更加友好姥宝。

今天就到這里了。希望下次還能給大家?guī)砀囿@喜恐疲。周五問答的主題是根據(jù)大家的想法產(chǎn)生的腊满,所以記得給我們寫信來提出你想要聽的話題套么。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán)碳蛋,最新文章請訪問 http://swift.gg违诗。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市疮蹦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌茸炒,老刑警劉巖愕乎,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異壁公,居然都是意外死亡感论,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門紊册,熙熙樓的掌柜王于貴愁眉苦臉地迎上來比肄,“玉大人,你說我怎么就攤上這事囊陡》技ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵撞反,是天一觀的道長妥色。 經(jīng)常有香客問我,道長遏片,這世上最難降的妖魔是什么嘹害? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮吮便,結(jié)果婚禮上笔呀,老公的妹妹穿的比我還像新娘。我一直安慰自己髓需,他們只是感情好许师,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著僚匆,像睡著了一般枯跑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上白热,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天敛助,我揣著相機與錄音,去河邊找鬼屋确。 笑死纳击,一個胖子當著我的面吹牛续扔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播焕数,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼纱昧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了堡赔?” 一聲冷哼從身側(cè)響起识脆,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎善已,沒想到半個月后灼捂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡换团,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年悉稠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片艘包。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡的猛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出想虎,到底是詐尸還是另有隱情卦尊,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布舌厨,位于F島的核電站猫牡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏邓线。R本人自食惡果不足惜淌友,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望骇陈。 院中可真熱鬧震庭,春花似錦、人聲如沸你雌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽婿崭。三九已至拨拓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氓栈,已是汗流浹背渣磷。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留授瘦,地道東北人醋界。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓竟宋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親形纺。 傳聞我的和親對象是個殘疾皇子丘侠,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

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

  • 我發(fā)現(xiàn)很多人在使用 Swift 時,都會抱怨 String API 很難用逐样。它很難學習并且設計得晦澀難懂蜗字,大多數(shù)人...
    HelloGeekBand閱讀 2,693評論 1 8
  • Swift學習有問必答群 : 313838956 ( mac版QQ有權(quán)限要求, 入群只能通過手機版 QQ申請...
    Guards翻譯組閱讀 6,591評論 9 13
  • 一個字符串 是一系列字符的集合,例如hello, world和albatross脂新。Swift的字符串是String...
    BoomLee閱讀 2,395評論 0 3
  • 本人天生不愛做飯挪捕。在家做人女兒時,有母親早晚操勞戏羽。婚后楼吃,天天上班始花,做飯全憑以前做大廚的公爹。偶而為之孩锡,飯不是多了就...
    南極雪北極冰閱讀 197評論 0 1
  • 昨天參加了林書宇導演的創(chuàng)作分享會酷宵,聽他回顧了《海巡尖兵》、《九降風》躬窜、《星空》浇垦、《百日告別》這一系列電影劇本寫...
    陳十五閱讀 615評論 2 2