第七章——字串(字串內部結構)

bestswifter發表於2017-12-27

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

本節將會描述Swift 2.0Beta2的String結構體的內部結構,僅供參考與更深入的理解。讀者不應該把它當成文件使用,因為隨著Swift的演變,String的內部結構隨時會發生變化。

在Swift 2.0中,呼叫sizeof(String)的結果是24,至少在64位的機器上結果如此。如果是在32位的機器上,結果是12。String的內部儲存結構由下面這樣的結構體組成:

struct Stringternals {
let buffer: UnsafePointer<Void>
let data: UInt
let copyTracker: UnsafePointer<Void>
}
複製程式碼

你可以自己建立字串變數,然後通過unsafeBitCast方法將它裝換成Stringternals結構體:

let hello = "hello"
let bits = unsafeBitCast(hello, Stringternals.self)
print(bits)

// 輸出結果:
// Stringternals(buffer: 0x0000000100297b10, data: 5, copyTracker: 0x0000000000000000)
複製程式碼

data表示字串長度,buffer是一個指標,指向實際儲存的一組ASCII字元,因此你可以用C語言的puts方法輸出這個buffer

// bits.buffer的型別是UnsafePointer<Void>,而puts方法的引數型別需要時UnsafePointer<Int8>
// 所以需要呼叫UnsafePointer的初始化方法做一個轉化
puts(UnsafePointer(bits.buffer))	// 輸出結果:hello
複製程式碼

輸出結構也有可能是字串hello以及後面的一堆垃圾資訊。這是因為buffer可以不用像C語言字串那樣以null結尾。

以上測試結果並不能說明Swift使用UTF-8編碼格式儲存字串,我們可以做以下實驗:

let hello = "hello,?"
let bits = unsafeBitCast(hello, Stringternals.self)
print(bits)

// 輸出結果:
// Stringternals(buffer: 0x000000010028fdf0, data: 9223372036854775816, copyTracker: 0x0000000000000000)
複製程式碼

輸出結果與此前有兩個區別:

  1. data的值是一個非常大的數:這是因為data現在不僅僅用來儲存字串的長度,它高位上的數用來儲存一個flag,表示字串包含了非ASCII的值,還有一個flag表示這個字串指向了一個NSString的buffer。我們可以通過新增一個計算屬性來遮蔽這些flag,以獲得字串的真實長度:
extension Stringternals {
var length: Int {
let mask = 0b11 << UInt(sizeof(UInt) * 8 - 2)
return Int(data & ~mask)
}
}

let hello = "hello,?"
let bits = unsafeBitCast(hello, Stringternals.self)

print(bits.length)	// 輸出結果:8
複製程式碼
  1. 第二個區別在於buffer指向的字元,現在都是16位的。一旦我們使用了一個或多個非ASCII字元就會導致Swift使用UTF-16編碼方式來儲存它們。這與非ASCII碼字元具體是什麼無關,也就是說即使它需要32位來儲存,Swift也會使用UTF-16編碼方式。

為了證明這一點,我們選擇一個4位元組(32位)的emoji,然後判斷Swift是否使用了UTF-16編碼方式。我們可以使用UTF-16.decode方法來解壓buffer:

let nonASCII = unsafeBitCast("hello,?", Stringternals.self)
let buf16 = UnsafeBufferPointer(start: UnsafePointer<UInt16>(nonASCII.buffer), count: nonASCII.length)
let buf32 = UnsafeBufferPointer(start: UnsafePointer<UInt32>(nonASCII.buffer), count: nonASCII.length)
var gen16 = buf16.generate()
var gen32 = buf32.generate()
var utf16 = UTF16()
var utf32 = UTF32()
print("UFT16解碼: ", terminator: "")
while case let .Result(scalar) = utf16.decode(&gen16) {
print(scalar, terminator: "")
}
print("\nUFT32解碼: ")
while case let .Result(scalar) = utf32.decode(&gen32) {
print(scalar)
}
複製程式碼

輸出結果如下:

UFT16解碼: hello,?
UFT32解碼:
複製程式碼

此前我們看到的copyTracker屬性一直是一個空指標,這是因為到目前為止我們都是用字串字面量來初始化字串,所以buffer指向的是二進位制檔案中的只讀資料區,如果我們使用字串的初始化方法:

let s1 = String("helllo")
let bits1 = unsafeBitCast(s1, Stringternals.self)
複製程式碼

這時的結果就不是空指標了,它是一個指向ARC管理的類引用的指標,與isUniquelyReferenceed函式聯合使用,為字串提供具備寫時複製特性的值語義。

這種字串內部結構的最後一個好處體現在字串切片方面。呼叫字串的split方法得到的其實是一組startingending指標,每一對指標對應了字串buffer內部的一個子陣列,於是就避免了不必要的複製。這麼做也是有代價的,在ARC機制下,即使一小片字串切片也會阻止整個字串被釋放,儘管這個字串的長度可能有好幾Mb。

如果你通過NSString建立String型別的變數,copyTracker還會使用一種優化方法,它實際上是指向原來NSString物件的引用,此時的buffer直接指向NSString的儲存區域:

let ns = "hello" as NSString
let s = ns as String
let (_,_,ref) = unsafeBitCast(s, (UInt,UInt,NSString).self)
print(ref)	// 輸出結果:hello
print(ref === ns)	// 輸出結果:true
複製程式碼

Character內部結構

在之前的文章中我們已經介紹過,Swift.Character表示了一組不確定長度的程式碼點,在Character結構體的內部如何管理這些程式碼點呢?嘗試執行下面這行程式碼:

print(sizeof(Character))	// 輸出結果:9
複製程式碼

奇數位長度的型別通常表示這是一個列舉型別——列舉成員佔用一位,其他的由關聯值佔用,所以Character結構體的內部大體上如下:

enum Character {
case ArbitraryLength(Buffer)
case Small(Int64)
}
複製程式碼

這種在內部儲存一小部分元素,然後切換到堆上的buffer上技術有事被稱作“小字串優化”。由於通常情況下字元長度大約是幾個位元組,所以在這種情況下,這種技術就特別適用。

需要強調一點,Character實際上是結構體而不是列舉型別,列舉型別只是其內部的一種私有表示,事實上,在命令列中輸入以下程式碼,你可以觀察到詳細的Character的內部結構:

echo ":type lookup Character" | swift | less
複製程式碼

這種技術適用於Swift中的任何型別,以便我們更深入的瞭解這個型別的內部實現。再重複一遍:這些東西不要當作文件使用,你應該總是依賴於Swift對外提供的文件。

相關文章