居然不能用整數(shù)下標(biāo)隨機(jī)訪問?
第一次使用Swift字符串之前,已經(jīng)習(xí)慣了C,C++直接通過下標(biāo)隨機(jī)訪問字符串?dāng)?shù)組的用法,乃至于第一次順手用Swift寫下這樣的代碼卻報(bào)錯(cuò)時(shí),滿腦子只剩黑人問號
let str = "hello world"
// 報(bào)錯(cuò):'subscript' is unavailable: cannot subscript String with an Int, see the documentation comment for discussion
let char = str[1]
跟隨著錯(cuò)誤提示,打開文檔才恍然大悟,String的下標(biāo)方法,根本不支持Int類型的下標(biāo):
subscript(bounds: Range<String.Index>) -> String { get }
Accesses the text in the given range.
subscript(i: String.Index) -> Character { get }
Accesses the character at the given position.
subscript(bounds: ClosedRange<String.Index>) -> String { get }
Accesses the text in the given range.
文檔中列出Swift標(biāo)準(zhǔn)庫只支持的三種下標(biāo)訪問String字符串的方法,看一下這三種參數(shù)。
-
Range<String.Index>:元素為String.Index類型的Range(開區(qū)間) -
String.Index:String.Index元素 -
ClosedRange<String.Index>:元素為String.Index類型的CloseRange(閉區(qū)間)
所以正確的String下標(biāo)訪問姿勢是這樣:
let str = "hello world"
/// 下標(biāo)類型為String.Index
let firstChar = str[str.startIndex] // h
let secondChar = str[str.index(after: str.startIndex)] // e
let thirdChar = str[str.index(str.startIndex, offsetBy: 2)]
let lastChar = str[str.index(before: str.endIndex)] // !
/// 下標(biāo)類型為Range<String.Index>
let fullStr = str[str.startIndex..<str.endIndex] // hello world!
/// 下標(biāo)類型為ClosedRange<String.Index>
let fullStr = str[str.startIndex...str.index(before: str.endIndex)] // hello world!
自己拓展下標(biāo)方法
學(xué)習(xí)了String的下標(biāo)方法之后,得知原來的錯(cuò)誤寫法應(yīng)該改成:
let str = "hello world"
//let char = str[1]
let char = str[str.index(str.startIndex, offsetBy: 1)]
很顯然比起直接用Int訪問的方式,寫起來麻煩很多,這個(gè)時(shí)候我們就可以利用拓展添加自己的下標(biāo)方法了
extension String {
subscript (i: Int) -> Character {
return self[self.index(self.startIndex, offsetBy: i)]
}
subscript (i: Int) -> String {
return String(self[i] as Character)
}
subscript (r: Range<Int>) -> String {
let start = index(startIndex, offsetBy: r.lowerBound)
let end = index(startIndex, offsetBy: r.upperBound)
return self[start..<end]
}
subscript (r: ClosedRange<Int>) -> String {
let start = index(startIndex, offsetBy: r.lowerBound)
let end = index(startIndex, offsetBy: r.upperBound)
return self[start...end]
}
}
用起來妥妥的很方便,追求安全的話也可以添加控制越界的代碼返回可選值,比如:
subscript (i: Int) -> Character? {
guard i < self.characters.count else{
return nil
}
return self[self.index(self.startIndex, offsetBy: i)]
}
想在Swift中直接使用整數(shù)進(jìn)行下標(biāo)訪問的時(shí)候,之前使用OC的小伙伴可能會想到這樣的辦法:
let str = "hello world"
//let char = str[1]
let char = (str as NSString).character(at: 1) //101
101是e在UTF-16(NSString對象使用UTF-16編碼)下的編碼,可見輸出正確,但是讓我們換一個(gè)字符串再看看:
let string = "e\u{301}" // é
let charFromNSString = (str as NSString).character(at: 0) //101
let charFromString = str[str.startIndex] //é
character at:方法與Swift的下標(biāo)訪問方法返回的值并不一樣,這是為什么呢?
Returns the character at a given UTF-16 code unit index.
這是Apple文檔中對character at:方法的描述,說明此方法的索引對象是字符串對應(yīng)的UTF-16碼元。所以返回了索引為0的碼元,即101。對于這種情況OC中有專門的字符串正規(guī)化處理辦法,也可以判斷一個(gè)字符的碼元長度,可以參考:NSString 與 Unicode中正規(guī)形式與隨機(jī)訪問部分。
至于為什么String類型就是直接輸出了é呢,這就要了解一下String類型與Unicode的關(guān)系了。
String背后的Unicode
Swift的String類型是基于Unicode標(biāo)量建立的,先來介紹一下Unicode和Unicode標(biāo)量。
Unicode
人類使用的文字和符號要想被計(jì)算機(jī)所理解必須要經(jīng)過編碼,Unicode就是其中的一種編碼標(biāo)準(zhǔn)。
碼點(diǎn):Unicode標(biāo)準(zhǔn)為世界上幾乎所有的書寫系統(tǒng)里所使用的每一個(gè)字符或符號定義了一個(gè)唯一的數(shù)字。這個(gè)數(shù)字叫做碼點(diǎn)(code points),以U+xxxx這樣的格式寫成,格式里的xxxx代表四到六個(gè)十六進(jìn)制的數(shù)。例如U+0061表示小寫的拉丁字母(LATIN SMALL LETTER A)("a"),U+1F425表示小雞表情(FRONT-FACING BABY CHICK) ("??"),有趣的是,字符的名字比如"FRONT-FACING BABY CHICK"也是Unicode標(biāo)準(zhǔn)的一部分。
編碼格式:通過字符到碼點(diǎn)之間的映射,人們得以用統(tǒng)一的方式表示符號,但還需要定義另一種編碼來確定碼點(diǎn)與其存儲在內(nèi)存和硬盤中的值的對應(yīng)關(guān)系。有三種Unicode支持的編碼格式:
- UTF-8:表示一個(gè)碼點(diǎn)需要1~4個(gè)八位的碼元。利用字符串的utf8屬性進(jìn)行訪問。
- UTF-16:用一或兩個(gè)16位的碼元表示一個(gè)嗎點(diǎn)。利用字符串的utf16屬性進(jìn)行訪問。
- 21位的 Unicode 標(biāo)量值集合,也就是字符串的UTF-32編碼格式,用21位的碼元表示一個(gè)碼點(diǎn)。利用字符串的unicodeScalars屬性進(jìn)行訪問。
String的可拓展字符群集
每一個(gè)String對象都有一個(gè)characters: String.CharacterView屬性,代表一個(gè)可拓展的字符群。Apple文檔中對String.CharacterView的描述:
In Swift, every string provides a view of its contents as characters. In this view, many individual characters—for example, “é”, “?”, and “????”—can be made up of multiple Unicode code points. These code points are combined by Unicode’s boundary algorithms into extended grapheme clusters, represented by the Character type. Each element of a CharacterView collection is a Character instance.
大意為每一個(gè)Swift字符串提供一個(gè)CharacterView包含它的全部內(nèi)容,在CharacterView中,如“é”, “?”, and “????”是作為獨(dú)立的character存在的,這些獨(dú)立的character可能是由多個(gè)Unicode碼點(diǎn)組成的。組成獨(dú)立character的一個(gè)或多個(gè)碼點(diǎn)會被Unicode組合成一個(gè)可拓展字符群,由Character類型來表示。CharacterView集合的每一個(gè)元素都是一個(gè)Character實(shí)例。先來看一下可拓展字形群的意思:
可拓展的字形群:一個(gè)或多個(gè)可生成人類可讀字符的Unicode標(biāo)量的有序排列。比如é即為一個(gè)可拓展字形群,它可以用單一的Unicode標(biāo)量é(LATIN SMALL LETTER E WITH ACUTE, 或者U+00E9)來表示。然而一個(gè)標(biāo)準(zhǔn)的字母e(LATIN SMALL LETTER E或者U+0065)加上一個(gè)急促重音(COMBINING ACTUE ACCENT,或者U+0301),這樣一對標(biāo)量就表示了同樣的字母é。在第一種情況,這個(gè)字形群包含一個(gè)單一標(biāo)量;而在第二種情況,它是包含兩個(gè)標(biāo)量的字形群。除此之外,還有幾個(gè)比較特殊的可拓展字符群集如:
let precomposed: Character = "\u{D55C}" // ?
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}" // ?, ?, ?
// precomposed 是 ?, decomposed 是 ?
let enclosedEAcute: Character = "\u{E9}\u{20DD}"
// enclosedEAcute 是 é?
let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
// regionalIndicatorForUS 是 ????
由文檔我們可以得知,CharacterView集合中的元素為Character實(shí)例,一個(gè)Character實(shí)例對應(yīng)一個(gè)可拓展字形群。所以Swift字符串無論是在計(jì)算長度(通過character的count屬性),還是在進(jìn)行下標(biāo)訪問,都是以Character,也就是可拓展字形群為最小單位的,所以在Swift字符串眼里é("\u{E9}")與e加 ?("\u{65}\u{301}")是一回事。
但NSString對象是以Unicode碼點(diǎn)為最小單位索引和計(jì)算長度,所以所示結(jié)果與Swift字符串不同。
let str = "e\u{301}" // é
let strLengthOfString = str.characters.count // 1
let strLengthOfNSString = (str as NSString).length // 2
參考書目與文章
《The Swift Programming Language:Strings and Characters》
NSString 與 Unicode