第七章——字串(字串與集合)

bestswifter發表於2017-12-27

本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。

如果檢視String結構體的定義就會發現其中有一個型別別名Index,有兩個屬性startIndexendIndexString型別還定義了下標指令碼,可以通過下標獲取對應位置上的字元。String實現了generate()函式,返回的生成器可以按順序遍歷所有字元。如果你對CollectionType協議比較熟悉,你會發現String結構體其實具備了實現這個協議的一切條件,不過因為它沒有顯示的宣告實現這個協議,所以我們無法使用for … in語法,String也無法呼叫定義在CollectionTypeSequenceType協議擴充裡的函式。理論上說我們可以自己擴充String結構體:

extension String: RangeReplaceableCollectionType {} // 宣告String實現了CollectionType協議,不需要提供任何方法實現。

var greeting: String = "Hello, world"
if let comma = greeting.indexOf(",") {
print(greeting[greeting.startIndex..<comma])
greeting.replaceRange(greeting.indices, with: "How about some original example string?")
}
複製程式碼

不過,顯然不是Swift語言的開發者忘記了讓String型別實現CollectionType協議。所以我們人為的去擴充String結構體是不明智的行為。雖然在CollectionTypeSequenceType協議擴充裡定義了很多非常有用的演算法,但並非所有演算法都適用於字串處理,因為其中涉及到Unicode字元的拼接問題。雖然Character型別儘可能把多個程式碼點拼接成一個字元,但有些時候一個字元一個字元地處理字串還是有可能會產生錯誤的結果。

因此,從“字元的集合”這一角度看到的字串,被宣告成了字串的一個屬性:characters,其它幾個類似的檢視有unicodeScalarsutf8utf16。選擇了某一個檢視後,你會進入集合處理模式,需要考慮你將要呼叫的演算法產生的結果。在所有檢視中,CharacterView是一個比較特殊的檢視。String的型別別名Index其實就是CharacterView.Index,這說明CharacterView檢視中的下標可以直接在字串型別中使用。

因為一個字元可以由不同數量的程式碼點組成,所以CharacterView檢視並非隨機訪問的,你無法判定一個給定的字元前有多少程式碼點。因此,String.Index只實現了BidirectionalIndexType,也就是我們可以從字串的起點(結尾)向後(前)遍歷,Swift會自動判斷有沒有多個程式碼點可以拼接併產生一個正確的偏移量。String的下標還實現了Comparable協議,你或許不知道兩個下標之間隔了多少字元,但至少可以知道一個字元的前一個和後一個字元是什麼。successorpredecessor方法提供了這個功能。

如果想要直接獲取到更遠距離的下標,可以使用advancedBy方法來代替在for迴圈中使用successorpredecessor方法:

let s = "abcdef"
let idx = s.startIndex.advancedBy(5, limit: s.endIndex)  // 向後5個位置
print(s[idx]) //輸出結果是:f
複製程式碼

limit引數可以確保不會越界,如果向後偏移量太大,超過了字串的結尾,idx的值就是s.endIndex。不過我們認為,advancedBy方法應該返回一個可選型別的值,如果越界就返回nil

有的讀者此時可能會恍然大悟:“我可以為String型別過載下標指令碼,接收整數型別的引數”,程式碼如下:

extension String {
subscript(idx: Int) -> Character {
let strIdx = self.startIndex.advancedBy(idx, limit: endIndex)
guard strIdx != endIndex else { fatalError("String index out of bounds") }
return self[strIdx]
}
}

print(s[5]) //輸出結果是:f
複製程式碼

正如我們不應該把字串擴充成集合型別,這種擴充最好也應該避免。這是因為,我們可能會寫出這樣的程式碼:

for i in 0..<5 {
print(s[i])
}
複製程式碼

但這種寫法的效率非常低下,我們知道下標是無法隨機訪問的,所以s[i]的時間複雜度是O(n),整個迴圈的時間複雜度是O(n ^ 2),隨著字串長度的增加,耗時會以平方級的速度增加。如果之前習慣了處理固定長度的字元,我們現在可能會覺得有些不適應:沒有了整數下標,怎麼方便快速地獲取到這些字元呢?好在Swift把字元放在集合型別中,我們可以像運算元組那樣處理String.characters

首先,即使沒有整數下標,遍歷所有的字元也很容易,可以使用for迴圈:

for (i, c) in "hello".characters.enumerate() {
print("\(i): \(c)")
}
複製程式碼

(i, c)是一個元組,i是從0開始的一組連續整數,c表示第i個字元。讀者可以執行這段程式碼,或直接檢視enumerate()函式的定義。

接下來,如果想要找到某一個特定的字元,我們可以使用indexOf方法:

var greeting: String = "Hello!"
if let idx = greeting.characters.indexOf("!") {
greeting.insertContentsOf(", world".characters, at: idx)
}
print(greeting)	// 輸出結果:Hello, world!
複製程式碼

String並不具備MutableCollectionType協議的特點,實現這個協議的集合的下標指令碼是可讀可寫的。這並不說明String不可變,我們只是無法使用下標指令碼修改String的某個字元。原因依然是字元的長度是不固定的,把一個由3個程式碼點組成的字元替換成由2個程式碼點組成的字元所需要的時間是線性的,因為後續所有的字元的位置都需要移動。因此,即使是替換單個字元,我們也需要使用replaceRange方法。

字串與可切片

處理陣列的切割操作是比較麻煩的,因為返回值的型別不是Array而是ArraySlice。這會讓寫遞迴函式變得痛苦,因為輸入的引數型別可能是ArraySlice。字串的集合檢視就沒有這樣的問題,SubSlic被定義成Self型別的一個例項,所以範型函式的引數型別可以是Sliceable,返回值也是Sliceable,舉個例子來說明,world變數的型別是String.CharacterView

let world = "Hello, world!".characters.suffix(6).dropLast()    // 值是:"world"
複製程式碼

陣列的split函式返回的是陣列切片,它對於字串處理也有用,其定義如下:

extension CollectionType {
public func split(maxSplit: Int = default,
allowEmptySlices: Bool = default,
@noescape isSeparator: (Self.Generator.Element) -> Bool) -> [Self.SubSequence]
}
複製程式碼

由於前兩個引數都有預設值,所以它的使用非常簡單:

let seperatedArray = "a,b,c".characters.split{ $0 == "," }.map { String($0) }
print(seperatedArray)	// 輸出結果:["a", "b", "c"]
複製程式碼

因為split函式的最後一個引數是閉包,所以它能做額不僅僅是比較字元這麼簡單,我們可以以此實現一個換行器,可以找到非常長的字串中的空格或換行符,然後自動換行:

extension String {
func wrap(after: Int = 70) -> String {
var i = 0
let lines = self.characters.split{ character -> Bool in
switch character {
case "\n", " " where i >= after:
i = 0
return true
default:
++i
return false
}
}.map(String.init)
return lines.joinWithSeparator("\n")
}
}
複製程式碼

因為split函式返回值的型別是[Self.SubSequence],所以對於字串來說,這個型別是String.CharacterView,因此需要呼叫map函式把陣列中的元素轉化成String型別。

在大多數時候,我們需要根據某個字元來分割字串,所以可以新增mSplit函式以簡化這樣的操作:

extension CollectionType where Generator.Element: Equatable {
func mSplit(seperator: Generator.Element) -> [SubSequence] {
return split { $0 == seperator}
}
}
複製程式碼

之前的程式碼簡化為:

let seperatedArray = "a,b,c".characters.mSplit(",").map { String($0) }
print(seperatedArray)	// 輸出結果:["a", "b", "c"]
複製程式碼

個人認為可以進一步簡化:

extension String {
func split(seperator: Character) -> Array<String> {
return characters.split{ $0 == seperator}.map { String($0) }
}
}

let seperatedArray = "a,b,c".split(",")
print(seperatedArray)	// 輸出結果:["a", "b", "c"]
複製程式碼

還可以定義一個split函式可以接受多個分隔符:

extension CollectionType where Generator.Element: Equatable {
func split<S: SequenceType where Generator.Element == S.Generator.Element>
(seperators: S) -> [SubSequence] {
return split { seperators.contains($0) }
}
}

print("Hello, world".characters.split(",!".characters).map { String($0) })
// 輸出結果:["Hello", " world"]
複製程式碼

相關文章