1. 程式人生 > >關於“幽靈架構”的補充說明2:Struct以及Copy

關於“幽靈架構”的補充說明2:Struct以及Copy

在“幽靈架構”Demo中我把兩個資料模型宣告成了Struct,蘋果WWDC2015的414號視訊講解了非常多關於Struct的優勢,其實也是所有值型別的優勢。首先Swift標準庫中絕大部分是值型別的,值型別的值傳遞是通過copy的,而作為一門靜態語言,Swift要求所有的物件都有明確的型別,明確的型別代表了固定的記憶體分配,而414號視訊也指出在記憶體中進行定長物件的copy是時間常數的,也就所謂的“cheap”。另外如果值型別的物件中包含引用型別的屬性的話,會破壞值型別的特性,出現共享(詳情請參考414號視訊),因此在Swift中,對於包含引用型別屬性的值型別物件間的Copy使用了Copy - on - Write這項技術,最基本的示例如下:

struct BezierPath {
    private var _path = UIBezierPath()

    var pathForReading: UIBezierPath {
        return _path  }

    var pathForWriting: UIBezierPath {
        mutating get {
            _path = _path.copy() as! UIBezierPath
            return _path
        }  
    } 
}

把引用型別的屬性宣告成private屬性,然後向外界暴露兩個公開的計算屬性,供讀取的計算屬性返回私有屬性本身,供寫入的計算屬性返回一個copy後的複本。這個版本的程式碼的問題是雖然保證了整個struct的值型別特性,但是每次寫入都需要copy效能並不高,因此在Copy - on - Write中的pathForWriting加入了一個方法:

struct MyWrapper {
    var _object: SomeSwiftObject
    var objectForWriting: SomeSwiftObject {
        mutating get {
            if !isUniquelyReferencedNonObjC(&_object)) {
                _object = _object.copy()
            }
            return _object
        }
    }
}

這裡的isUniquelyReferencedNonObjC方法會判斷你當前訪問的MyWrapper的_object屬性是否只有一個引用,如果是通過 let b = a 這樣的方法建立的話,那麼a的_object和b的_object的引用會指向同一個地址,在讀取值的時候不關心_object物件的引用,如果只進行讀取的話是不需要進行copy的,如果需要寫入b的_objc話需要訪問objectForWriting,此時objectForWriting會檢查_object的引用,如果如let b = a 的方式建立的b物件,那麼b中_object的引用是2,此時會將b的_object物件賦成_object拷貝後的副本,這樣在之後繼續訪問objectForWriting的時候由於b中的_object已經是新的物件了,引用只有1,所以_object會被直接返回。Swift中的很多會包含引用物件的值物件都採用了Copy-on-Write技術,比如我們常用的陣列。需要注意的是這裡的引用型別的物件必須是Swift物件,如果你使用了一個OC中的引用型別,那麼你需要對其進行一個封裝。
用法如下:

 final class Box<A> { 
     var unbox: A
    init (_ value: A) { unbox = value } 
}

其中A是非Swift原生物件,在類中宣告某個物件的時候使用”Box”,在取值的時候需要呼叫Box例項的unbox屬性獲得原始的物件。除了使用“=”會發生copy外,向一個方法中傳入值型別時也會發生copy,所以可以安全地操作傳入的引數。通常情況下Swift中不會直接操作一個指標,所有的指標都會被標註為“unsafe”,如果想要方法改變傳入的引數,可以把該引數宣告成inout,然後在傳入時引數前加&,&代表傳入的是一個地址,inout的形式與直接操作某個物件的指標看起來是相同的,但是inout其實依舊使用了copy,不同的是在方法體結束的時候會把處理後的copy物件的值再賦回給原始的物件,請看下面的例子:

let clo:Int -> Void = { i in print("形參的值\(i)")
}
func method(inout num:Int){
    clo(num)
    num += 1
}
method(&a)
print("a的值:\(a)")

我們知道閉包可以捕獲物件,閉包中捕獲的是形參物件num,如果閉包num和a指向同一個地址的話,那麼在method方法體中clo捕獲的應該是a的引用,但其實clo捕獲的只是copy後的副本,而已,在a發生變化之前就已經將副本捕獲了,導致最後的列印結果不同。

下面來聊聊Struct,所有屬性的型別都確定的Struct會被儲存在棧上,Swift中每個Struct型別的長度是固定的,好比每個指標都是8位元組,所以指標會被儲存在棧上。在使用引用型別的時候,每次從棧上取到對應的指標需要根據指標提供的地址資訊去堆上尋找引用型別的真實值,定長的Struct的值會被直接儲存在棧上,這就省去了定址的開銷,棧的空間是有限的,如果一組值型別的長度超過了棧的空間,那麼會被自動轉移到堆上。
讓我們在playground上寫一些示例:

struct StructDemo{
    var a = 1
    var str = ""
}
let strDemo = StructDemo()
sizeofValue(strDemo)
sizeof(StructDemo)

這裡Int的長度是8位元組,String的長度是24位元組,最終StructDemo的長度是32,完全取決於其屬性的長度,sizeof和sizeofValue分別列印型別和例項的長度,這兩個方法顯示的都是棧上的長度,結果是相同的:
這裡寫圖片描述

如果換成用class宣告,長度是8。