本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。
在本節中,我們會通過自己實現一個正規表示式匹配器來說明“字串切片依然是字串”這一特性的用途。首先定義一個Regex
結構體:
public struct Regex {
private let regexp: String
public init(_ regexp: String) {
self.regexp = regexp
}
}
複製程式碼
它由一個常量屬性regexp
和初始化方法構成。regexp
字串用於儲存正規表示式。因為我們只是實現一個非常簡單的匹配器,所以初始化函式中不會判斷正規表示式是否有效(總是預設是有效的),也就不需要定義一個可失敗構造器了。
為了簡單起見,我們實現的匹配器只能匹配^
、$
以及*
、.
這四種元字元。為了方便讀者理解下面的程式碼,首先解釋一下這四種元字元的作用:
^
:匹配輸入字串的開始位置。比如^a
可以匹配所有首字母為a
的字串,因此它可以匹配字串abcd
,但不能匹配字串dcba
$
:匹配輸入字串的結束位置。比如d$
可以匹配所有以字母d
結尾的字串,因此它可以匹配字串abcd
,但不能匹配字串dcba
*
:匹配前面的子表示式任意次。比如t*a
可以匹配任意多個字母t
後面加上字母a
,因此它可以匹配abc
(0個t
),可以匹配ttabc
(2個t
),但是不能匹配ttba
。.
:匹配單個任意單個字元。比如.a
可以匹配aa
、ba
、ca
等,但是不能匹配ab
。
接下來,我們擴充Regex
結構體,實現match
函式。引數是待匹配的字串,如果能匹配則返回true
:
extension Regex {
public func match(text: String) -> Bool {
//如果regex以^開頭,只能從引數的第一個字元開始匹配
if regexp.characters.startsWith("^".characters) {
return Regex.matchHere(regexp.characters.dropFirst(), text.characters)
}
//依次從不同的位置開始,嘗試匹配每一個子串
for var idx = text.startIndex; ; ++idx {
if Regex.matchHere(regexp.characters, text.characters[idx..<text.endIndex]) {
return true
}
if idx == text.endIndex {break}
}
return false
}
}
複製程式碼
整個函式的功能並不複雜,只是迭代了字串的每一個可能子串並嘗試匹配。主要的匹配邏輯在靜態函式matchHere
中。注意,在這裡我們已經處理了^
的匹配。接下來是matchHere
的實現:
extension Regex {
// 從text引數的開始位置匹配
private static func matchHere(regexp: String.CharacterView, _ text: String.CharacterView) -> Bool {
// 空的正規表示式可以匹配所有字串
if regexp.isEmpty { return true }
// *之前的字元需要呼叫matchStar函式
if let c = regexp.first where regexp.dropFirst().first == "*" {
return Regex.matchStar(c, regexp.dropFirst().dropFirst(), text)
}
// 如果當前正規表示式中只有一個字元$,當前僅當text也為空時才能匹配
if regexp.first == "$" && regexp.dropFirst().isEmpty {
return text.isEmpty
}
// 如果第一個字元匹配,就從後面的字元開始,繼續匹配
if let tc = text.first, let rc = regexp.first where rc == "." || tc == rc {
return matchHere(regexp.dropFirst(), text.dropFirst())
}
return false
}
}
複製程式碼
這個函式有四個if語句,分別處理四種情況。第一個if語句表示空正規表示式可以匹配所有字串,第二個if語句處理*
的匹配,待會兒詳細分析。第三個if語句處理$
元字元的匹配,最後一個則是正常的匹配邏輯。因為*
可以匹配0到多個字元,它被單獨提取出來處理:
extension Regex {
private static func matchStar(c: Character,
_ regexp: String.CharacterView,
_ text: String.CharacterView) -> Bool {
var idx = text.startIndex
repeat { // *可以匹配前面的表示式0次或多次
if matchHere(regexp, text[idx..<text.endIndex]) {
return true
}
} while idx != text.endIndex && (text[idx++] == c || c == ".")
return false
}
}
複製程式碼
定義完了以後,我們來實際使用體驗一下,匹配很簡單:
print(Regex("^h..lo*!$").match("hellooooo!")) // 輸出結果是:true
複製程式碼
這個正規表示式的含義如下,顯然匹配結果是true
以字母h開頭,第四個字母是l,隨後有0到多個字母o,以!結尾
在實現匹配器的過程中,我們大量的使用了字串切片,比如下標指令碼和dropFirst
函式,以及可選型別和非可選型別直接的比較。比如這樣的程式碼if regexp.first == "^"
,即使字串為空字串也可以正常執行。此時"".first
的值是nil
,而nil
與非可選型別變數的==
運算結果總是false
以上程式碼最醜陋的部分可能是for迴圈的使用(如果你讀過此書的前面章節,你幾乎不會看到for ;;
這種語法的出現)。導致程式碼很醜的原因是我們要遍歷字串的所有子串,包括最後的空字串,否則就可以把idx != text.endIndex
寫在for迴圈裡了。要包括空字串是因為確保Regex("$").match("abc")
的返回值為true
。如果字串能像陣列那樣可以使用整數下標,我們就可以這樣實現:
for idx in text.startIndex...text.endIndex {
if Regex.matchHere(regexp.characters, text.characters[idx..<text.endIndex]) {
return true
}
}
複製程式碼
此時,最後一次迴圈中,idx
的值是text.endIndex
,所以text.characters[idx..<text.endIndex]
是一個空字串。不幸的是這樣寫會報錯。
我們知道定義區間有兩種方式:..<
表示右側開區間,...
表示右側閉區間。但對應到Range
型別其實只有一個,比如a..<b
對應到Range
型別被表示為Range(start: a, end: b)
,而a...b
會被轉換成Range(start: a, end: b.successor())
。對於字串來說,呼叫endIndex.successor()
會導致程式崩潰,而陣列則不存在這樣的問題:
let x = [1,2,3]
print(x.endIndex) // 輸出結果:3
print(x.endIndex.successor()) // 輸出結果:4
let s = "abc"
print(s.endIndex) // 輸出結果:3
print(s.endIndex.successor()) // fatalError: can not increment endIndex
複製程式碼
陣列使用的是整數下標,即使獲取了endIndex
的後繼也不會出大問題,但字串下標就要複雜的多了。還是老原因,因為組成一個字元的程式碼點數量不確定,陣列下標在增加時必須讀取記憶體才能知道向後偏移多少。因此呼叫endIndex.successor()
就會導致訪問到不該訪問的記憶體。
因此,如果使用text.startIndex...text.endIndex
會導致程式立刻崩潰。之前的C語言風格的for迴圈雖然醜,但也只能這樣解決問題。