為文本字符串添加特性或者語法糖在各種編程語言中都很普遍。就拿大家都很熟悉的 C 語言舉例,C 字符串本質(zhì)是一個(gè)字符數(shù)組(characters array),但是每次輸入字符串的時(shí)候不用輸入 ['h','e','l','l','o'] ,直接打 hello 就可以了,因?yàn)檫@個(gè)操作編譯器幫你做了。
更高級的語言比如 Swift 處理字符串就不僅僅是當(dāng)做字符數(shù)組了,String 是一個(gè)完整的類型,并且有各種特性。我們先來看一下 String 的一個(gè)特性:substring。
簡單的看一下 String
首先粗略的了解一下字符串的實(shí)現(xiàn)。下面的代碼來自標(biāo)準(zhǔn)庫中 String.swift:
public struct String {
public var _core: _StringCore
}
當(dāng)然也有一些其他初始化設(shè)置,不過在聲明里只有這一個(gè)存儲屬性!秘密一定都在 StringCore.swift 里:
public struct _StringCore {
public var _baseAddress: UnsafeMutableRawPointer?
var _countAndFlags: UInt
public var _owner: AnyObject?
}
在這個(gè)類型里還有很多其他東西,不過我們還是只關(guān)注存儲屬性:
- Base address — 一個(gè)指向內(nèi)部存儲的指針
- Count — 字符串長度,UInt 類型,在一個(gè) 64 位的系統(tǒng)中,意味著有 62(64 - 2) 位的空間可以表示長度。這是一個(gè)非常大的數(shù)字。所以字符串的長度不太可能溢出。
- Flags — 兩個(gè) bits 用來做標(biāo)志。第一位表示是否被 _StringBuffer 持有;第二位表示編碼格式是 ASCII 還是 UTF-16。
_StringCore 的真實(shí)情況比這里提到的要復(fù)雜的多,但是通過上面的內(nèi)容可以讓我們更容易理解字符串的一些信息:字符串的內(nèi)部存儲和存儲的大?。╱nderlying storage and size)。
Substring
Swift 中要怎么創(chuàng)建一個(gè) substring?最簡單的方式就是通過下標(biāo)從 string 取一段:
let str = "Hello Swift!"
let slice = str[str.startIndex..<str.index(str.startIndex, offsetBy: 5)]
// "Hello"
雖然很簡單,但是代碼看起來不太優(yōu)雅。
String 的索引不是直觀的整型,所以截取時(shí)的位置索引需要利用 startIndex 和 index(_:offsetBy:)獲取。如果是從字符串開始位置截取,可以省略掉 startIndex :
let withPartialRange = str[..<str.index(str.startIndex, offsetBy: 5)]
// still "Hello"
或者用 collection 中的這個(gè)方法:
let slice = str.prefix(5)
// still "Hello"
要記住字符串也是 collection ,所以你可以用集合下的方法,比如 prefix(),suffix(), dropFirst() 等。
Substring 的內(nèi)部原理
substring 一個(gè)神奇的地方是他們重用了父 string 的內(nèi)存。你可以把 substring 理解為父 string 的其中一段。

舉個(gè)例子,如果從一個(gè) 8000 個(gè)字符的字符串中截取 100 個(gè)字符,并不需要重新初始化 100 個(gè)字符的內(nèi)存空間。
這也意味著你可能不小心就把父 string 的生命周期延長了。如果有一大段字符串,然后你只是截取了一小段,只要截取的小段字符串沒有釋放,大段的字符串也不會被釋放。
Substring 內(nèi)部到底是怎么做到的呢?
public struct Substring {
internal var _slice: RangeReplaceableBidirectionalSlice<String>
內(nèi)部的 _slice 屬性保存著所有關(guān)于父字符串的信息:
// Still inside Substring
internal var _wholeString: String {
return _slice._base
}
public var startIndex: Index { return _slice.startIndex }
public var endIndex: Index { return _slice.endIndex }
計(jì)算屬性 _wholeString(返回整個(gè)父字符串),startIndex 和 endIndex 都是通過內(nèi)部的 _slice 返回。
也可以看出 slice 是如何引用父字符串的。
Substring 轉(zhuǎn)換為 String
最后代碼里可能有很多 substring,但是函數(shù)的參數(shù)類型需要的是 string。Substring 轉(zhuǎn)換到 string 的過程也很簡單:
let string = String(substring)
因?yàn)?substrings 和它的父字符串共享同一個(gè)內(nèi)存空間,猜測創(chuàng)建一個(gè)新字符串應(yīng)該會初始化一片新的存儲空間。那么 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)
}
}
}
- 創(chuàng)建一個(gè)對原有父字符串的引用
- 獲取 substring 在父字符串中的開始和結(jié)束位置
- 獲取 UTF-16 格式的 substring 內(nèi)容。_core 是 _StringCore 的一個(gè)實(shí)例。
- 判斷匹配的 unicode 編碼,生成一個(gè)新的字符串實(shí)例
把 substring 轉(zhuǎn)換成 string 的步驟非常簡單,但是你可能要考慮是不是一需要這樣做。是不是進(jìn)行 substring 操作的時(shí)候都要求類型是 string?如果對 substring 的操作都需要轉(zhuǎn)成 string,那么輕量級的 substring 也就失去了意義。??
StringProtocol
StringProtocol 上場!StringProtocol 真是面向協(xié)議編程的一個(gè)優(yōu)秀代表。StringProtocol 抽象了字符串的常見功能,比如 uppercased(), lowercased(),還有 comparable、collection 等。String 和 Substring 都聲明了 StringProtocol。
也就是說你可以直接使用 == 對 substring 和 string 進(jìn)行判等,不需要類型轉(zhuǎn)換:
let helloSwift = "Hello Swift"
let swift = helloSwift[helloSwift.index(helloSwift.startIndex, offsetBy: 6)...]
// comparing a substring to a string
swift == "Swift" // true
也可以遍歷 substring,或者從 substring 截取子字符串。
在標(biāo)準(zhǔn)庫里也有一小部分函數(shù)使用 StringProtocol 類型作為參數(shù)。比如把一個(gè)字符串轉(zhuǎn)換為整型就是:init(text: StringProtocol)。
雖然你可能不關(guān)心是 string 和 substring,但是使用 StringProtocol 作為參數(shù)類型,調(diào)用者就不用進(jìn)行類型轉(zhuǎn)換,對他們會友好很多。
總結(jié)
- 字符串還是那個(gè)常見的字符串。
- Substring 是字符串的一部分,和父字符串共享同一塊內(nèi)存空間,并且記錄了自己的開始和結(jié)束位置。
-
String 和 Substring 都聲明實(shí)現(xiàn)了 StringProtocol。StringProtocol 包含了一個(gè)字符串的基本屬性和功能。
是不是覺得自己也可以自定義字符串類型,實(shí)現(xiàn) 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
但是蘋果爸爸表示了拒絕。
如果想與我有更密切的交流也可以加入我的知識星球:程序員生存指南
