原文Swift Substrings莺禁,作者Greg Heo
(sub)string不是字符串?是也不是
文本字符串非常常用,編程語言通常對其提供特殊特性或者語法糖。以大家最常用的C語言來說拷淘,其字符串只是字符數(shù)組。但是你不需要創(chuàng)建一個數(shù)組['h','e','l','l','o']
指孤,只要輸入"hello"
編譯器就會打點好一切启涯。
更高級的語言比如Swift中贬堵,字符串不僅僅是簡單的字符數(shù)組,而是一個完整的類型并且有著各種特性结洼。在這次的string觀察中黎做,我們關(guān)注他的一小部分行為:substring。
String松忍,簡述
首先蒸殿,我們快速遍歷一次string的實現(xiàn)。以下來自標準庫的 String.swift 文件:
public struct String {
public var _core: _StringCore
}
里面還有很多的構(gòu)造函數(shù)鸣峭,不過整個主類型里只有一個存儲屬性宏所!好東西一定都在 StringCore.swift 里:
public struct _StringCore {
public var _baseAddress: UnsafeMutableRawPointer?
var _countAndFlags: UInt
public var _owner: AnyObject?
}
這個類型里面依然很多東西,我們還是只關(guān)注存儲屬性:
- Base address — 指向底層存儲的原始指針叽掘。
-
Count — 字符串長度楣铁,存儲在
_countAndFlags
的低位(UInt.bitWidth - 2)
玖雁。 在64位環(huán)境下更扁,這表示最大可用62位表示字符串長度,即 4 x 10^18 — 整整4 EB字節(jié)赫冬! -
Flags — 兩個標志位:一位表示字符串底層使用Swift原生的
_StringBuffer
浓镜,或者是NSString
形式的 Cocoa buffer;第二位表示buffer存儲的編碼是ASCII還是UTF-16劲厌。
_StringCore
還有更多的復雜細節(jié)膛薛,這樣快速過一遍就能讓我們找到想要的:string有底層存儲以及大小。
Substring
Swift中如何創(chuàng)建一個substring补鼻?最簡單的辦法是通過下標對string切片:
let str = "Hello Swift!"
let slice = str[str.startIndex..<str.index(str.startIndex, offsetBy: 5)]
// "Hello"
好吧哄啄,很簡單,但是代碼不太好看??风范。
string的索引不使用整數(shù)咨跌,而是靠startIndex
和index(_:offsetBy:)
操作。既然我們從startIndex
開始硼婿,那么可以靠partial range簡化一下代碼锌半。
let withPartialRange = str[..<str.index(str.startIndex, offsetBy: 5)]
// still "Hello"
或者用collection(容器)的切片方法:
let slice = str.prefix(5)
// still "Hello"
記住,string是容器寇漫,你可以使用所有容器常用的方法刊殉,比如 prefix()
, suffix()
州胳,dropFirst()
记焊,等等。
Substring內(nèi)部
substring的一部分魔法是重用了“parent” string的內(nèi)存栓撞。你可以認為substring由base string和range構(gòu)成遍膜。
這意味著從一個8000字符的string上切一個100字符的substring不需要分配額外內(nèi)存,也不用復制100個字符。
這也意味著你可能無意延長了你的base string的生命周期捌归。如果你有一個超大字符串存了一整本小說肛响,然后從其中切片了一個單詞,那么只要substring還在惜索,這個巨大的string就會一直陰魂不散特笋。
那么到底substring內(nèi)部是如何跟蹤這些的呢?
public struct Substring {
internal var _slice: RangeReplaceableBidirectionalSlice<String>
內(nèi)部的 _slice
屬性保存了關(guān)于base string的所有信息:
// 仍然是 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
(指出在string中切片的范圍)只是簡單的傳遞了內(nèi)部切片屬性的值猎物。
你也能看到切片如何用 _base
來保存原始string的信息。
從Substring到String
所以你有了一堆各種各樣的的substring角塑,而你的函數(shù)需要的是string蔫磨。怎么辦?不用擔心圃伶,把substring轉(zhuǎn)換成string很簡單:
let string = String(substring)
既然substring和他們的base string共享內(nèi)存堤如,創(chuàng)建一個新的string理當分配新的內(nèi)存。那么String
中這個接受substring的構(gòu)造函數(shù)中有發(fā)生了什么窒朋?
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)
}
}
}
獲取完整base string的引用搀罢。
獲取開始和結(jié)束范圍的索引。
獲取切片的UTF-16表現(xiàn)形式侥猩。
_core
是一個_StringCore
實例榔至,而encodedOffset
屬性是string的UTF-16版本的索引。-
檢查索引是否與unicode標量匹配欺劳,分支4A表示表示你沒有不成對的代理碼 (Unicode is hard)唧取,因而可以使用基于
_StringCore
的UTF-16 buffer切片直接創(chuàng)建新的字符串。否則划提,走分支4B重新以UTF-16編碼buffer枫弟,使用
init(decoding:as:)
初始化string。無論如何腔剂,最后都得到一個新的String
實例媒区。
把substring轉(zhuǎn)換為string非常簡單,不過真的是必須嘛掸犬?每次使用的時候都要把subtstring用String()
包起來袜漩?那樣的話我們使用輕量化的substring所得到的效率不就白費了?
StringProtocol
來看看 StringProtocol
湾碎!作為面向協(xié)議編程的絕佳例子宙攻, StringProtocol
抽象了字符串的函數(shù)功能,比如uppercased()
介褥,lowercased()
座掘, comparable递惋,hashable,collection溢陪,等等萍虽。 String
及Substring
都遵循StringProtocol
。
這意味著你可以用 ==
比較string和substring形真,而不用先轉(zhuǎn)換substring:
let helloSwift = "Hello Swift"
let swift = helloSwift[helloSwift.index(helloSwift.startIndex, offsetBy: 6)...]
// 直接比較 substring 和 string ??
swift == "Swift" // true
你也可以遍歷substring杉编,以及從substring再取一個substring。
你會發(fā)現(xiàn)標準庫里有一些函數(shù)接受StringProtocol
而非String
咆霜,比如把string轉(zhuǎn)換成整數(shù)和浮點使用的構(gòu)造器init(text: StringProtocol)
邓馒。
也許你自己的代碼里,你不在乎處理的是string還是substring蛾坯?那樣的話光酣,考慮接受 StringProtocol
參數(shù),那樣調(diào)用者就不用在傳參的時候再手工轉(zhuǎn)換一次了脉课。
結(jié)語
小結(jié)一下:
- String 還是字符串救军,始終如一。
- Substring 是string的切片下翎。它們與base string共享內(nèi)存buffer缤言,并擁有一對范圍索引宝当。
-
StringProtocol 抽取出字符串的特征以及如何訪問其功能视事,放進一個協(xié)議中。
String
及Substring
都遵循StringProtocol
庆揩。
所以有誰準備好寫一個自己的字符串類型了俐东?搞一個你自己StringProtocol
實現(xiàn)來加入string聚會???
/// 不要定義新的 `StringProtocol`實現(xiàn)订晌。 標準庫中只有 `String` 和
/// `Substring` 是有效的實現(xiàn)虏辫。
public protocol StringProtocol
那好吧,我們還是來搞自己的 boolean類型好了吧锈拨???