為文字字串新增特性或者語法糖在各種程式語言中都很普遍。就拿大家都很熟悉的 C 語言舉例,C 字串本質是一個字元陣列(characters array),但是每次輸入字串的時候不用輸入 ['h','e','l','l','o']
,直接打 hello
就可以了,因為這個操作編譯器幫你做了。
更高階的語言比如 Swift 處理字串就不僅僅是當做字元陣列了,String 是一個完整的型別,並且有各種特性。我們先來看一下 String 的一個特性:substrings。
簡單的看一下 Strings
首先粗略的瞭解一下字串的實現。下面的程式碼來自標準庫中 String.swift:
public struct String {
public var _core: _StringCore
}
複製程式碼
當然也有一些其他初始化設定,不過在宣告裡只有這一個儲存屬性!祕密一定都在 StringCore.swift 裡:
public struct _StringCore {
public var _baseAddress: UnsafeMutableRawPointer?
var _countAndFlags: UInt
public var _owner: AnyObject?
}
複製程式碼
在這個型別裡還有很多其他東西,不過我們還是隻關注儲存屬性:
- Base address — 一個指向內部儲存的指標
- Count — 字串長度,UInt 型別,在一個 64 位的系統中,意味著有 62(64 - 2) 位的空間可以表示長度。這是一個非常大的數字。所以字串的長度不太可能溢位。
- Flags — 兩個 bits 用來做標誌。第一位表示是否被 _StringBuffer 持有;第二位表示編碼格式是 ASCII 還是 UTF-16。 _StringCore 的真實情況比這裡提到的要複雜的多,但是通過上面的內容可以讓我們更容易理解字串的一些資訊:字串有一些內部儲存和儲存的大小(underlying storage and size)。
Substrings
Swift 中要怎麼建立一個 substring?最簡單的方式就是通過下標從 string 取一段:
let str = "Hello Swift!"
let slice = str[str.startIndex..<str.index(str.startIndex, offsetBy: 5)]
// "Hello"
複製程式碼
雖然很簡單,但是程式碼看起來不太優雅?。
String 的索引不是直觀的整型,所以擷取時的位置索引需要利用 startIndex 和 index(_:offsetBy:)
獲取。如果是從字串開始位置擷取,可以省略掉 startIndex :
let withPartialRange = str[..<str.index(str.startIndex, offsetBy: 5)]
// still "Hello"
複製程式碼
或者用 collection 中的這個方法:
let slice = str.prefix(5)
// still "Hello"
複製程式碼
要記住字串也是 collection ,所以你可以用集合下的方法,比如 prefix(),suffix(), dropFirst() 等。
Substring 的內部原理
substring 一個神奇的地方是他們重用了父 string 的記憶體。你可以把 substring 理解為父 string 的其中一段。
舉個例子,如果從一個 8000 個字元的字串中擷取 100 個字元,並不需要重新初始化 100 個字元的記憶體空間。 這也意味著你可能不小心就把父 string 的生命週期延長了。如果有一大段字串,然後你只是擷取了一小段,只要擷取的小段字串沒有釋放,大段的字串也不會被釋放。 Substring 內部到底是怎麼做到的呢?
public struct Substring {
internal var _slice: RangeReplaceableBidirectionalSlice<String>
複製程式碼
內部的 _slice 屬性儲存著所有關於父字串的資訊:
// Still inside Substring
internal var _wholeString: String {
return _slice._base
}
public var startIndex: Index { return _slice.startIndex }
public var endIndex: Index { return _slice.endIndex }
複製程式碼
計算屬性 _wholeString(返回整個父字串),startIndex 和 endIndex 都是通過內部的 _slice 返回。 也可以看出 slice 是如何引用父字串的。
Substring 轉換為 String
最後程式碼裡可能有很多 substring,但是函式的引數型別需要的是 string。Substring 轉換到 string 的過程也很簡單:
let string = String(substring)
複製程式碼
因為 substrings 和它的父字串共享同一個記憶體空間,猜測建立一個新字串應該會初始化一片新的儲存空間。那麼 string 的初始化到底過程是怎樣的呢。
extension String {
public init(_ substring: Substring) {
// 1
let x = substring._wholeString
// 2
let start = substring.startIndex
let end = substring.endIndex
// 3
let u16 = x._core[start.encodedOffset..<end.encodedOffset]
// 4A
if start.samePosition(in: x.unicodeScalars) != nil
&& end.samePosition(in: x.unicodeScalars) != nil {
self = String(_StringCore(u16))
}
// 4B
else {
self = String(decoding: u16, as: UTF16.self)
}
}
}
複製程式碼
- 建立一個對原有父字串的引用
- 獲取 substring 在父字串中的開始和結束位置
- 獲取 UTF-16 格式的 substring 內容。_core 是 _StringCore 的一個例項。
- 判斷匹配的 unicode 編碼,生成一個新的字串例項 把 substring 轉換成 string 的步驟非常簡單,但是你可能要考慮是不是一需要這樣做。是不是進行 substring 操作的時候都要求型別是 string?如果對 substring 的操作都需要轉成 string,那麼輕量級的 substring 也就失去了意義。?
StringProtocol
StringProtocol 上場!StringProtocol 真是面向協議程式設計的一個優秀代表。StringProtocol 抽象了字串的場景功能,比如 uppercased()
, lowercased()
,還有 comparable
、collection
等。String 和 Substring 都宣告瞭 StringProtocol。
也就是說你可以直接使用 ==
對 substring 和 string 進行判等,不需要型別轉換:
let helloSwift = "Hello Swift"
let swift = helloSwift[helloSwift.index(helloSwift.startIndex, offsetBy: 6)...]
// comparing a substring to a string ?
swift == "Swift" // true
複製程式碼
也可以遍歷 substring,或者從 substring 擷取子字串。
在標準庫裡也有一小部分函式使用 StringProtocol 型別作為引數。比如把一個字串轉換為整型就是:init(text: StringProtocol)
。
雖然你可能不關心是 string 和 substring,但是使用 StringProtocol 作為引數型別,呼叫者就不用進行型別轉換,對他們會友好很多。
總結
- 字串還是那個常見的字串。
- Substring 是字串的一部分,和父字串共享同一塊記憶體空間,並且記錄了自己的開始和結束位置。
- String 和 Substring 都宣告實現了 StringProtocol。StringProtocol 包含了一個字串的基本屬性和功能。
是不是覺得自己也可以自定義字串型別,實現 StringProtocol ?
/// Do not declare new conformances to `StringProtocol`. Only the `String` and
/// `Substring` types in the standard library are valid conforming types.
public protocol StringProtocol
複製程式碼
但是蘋果爸爸表示了拒絕。