Swift Talk:理解值類型

我們使用寫時復(fù)制 copy on write 的思想,對 NSMutableData 進行封裝,以此來理解我們的標準庫的實現(xiàn)方式。

標準庫中提供的所有的基本集合類型都是值類型,通過寫時復(fù)制的思想保證了他的高效性。集合類型是我們比較常用到的數(shù)據(jù)類型,所以了解他的性能特性很重要,我們來一起看一下寫時復(fù)制是如何工作的,并且嘗試自己手動實現(xiàn)一個。

引用類型

舉個例子,我們比較一下Swift的Data(結(jié)構(gòu)體)和Foundation庫中的NSMutableData(類)。首先我們使用一些字節(jié)數(shù)據(jù)來初始化 NSMutableData 實例。

var sampleBytes: [UInt8] = [0x0b,0xad,0xf0,0x0d]
let nsData = NSMutableData(bytes: sampleBytes, length: sampleBytes.count)

我們使用了 let 來聲明 nsData,但是像 NSMutableData 這樣的引用類型不收let/var 的控制。對于引用類型來說,用 let 聲明代表 nsData 這個指針不能在指向別的內(nèi)存,但是他指向的這個內(nèi)存中的數(shù)據(jù)是可以變化的。也就是說我們依然可以往 nsData 中 append 數(shù)據(jù)。

nsData.append(sampleBytes, length: sampleBytes.count)

當我們再聲明一個對象,改變其中一個對象,另一個對象也會發(fā)生變化。

let nsOtherData = nsData
nsData.append(sampleBytes, length: sampleBytes.count)
// nsOtherData 也會變

如果我們想產(chǎn)生一個獨立的副本,我們需要使用 mutableCopy(返回一個 Any 類型),我們需要把返回值強轉(zhuǎn)成我們需要的 NSMutableData 類型。

let nsOtherData = nsData.mutableCopy() as! NSMutableData
nsData.append(sampleBytes, length: sampleBytes.count)
// nsOtherData 不變

值類型

首先我們也是通過 sampleBytes 來初始化一個 Data。

let data = Data(bytes: sampleBytes, count: sampleBytes.count)

如果我們使用 let 關(guān)鍵字,那編譯器就不會允許我們調(diào)用類型 append 這樣的方法。所以如果要改變 data 的值,要使用 var 。

var data = Data(bytes: sampleBytes, count: sampleBytes.count)
data.append(contentsOf: sampleBytes)

Data 和 NSData 最主要的不同之處是:把值賦給另一個變量時或者作為參數(shù)傳到方法中,Data 總是會生成一個新的副本,但是 NSData 只會生成一個新的引用,但是兩個引用指向同一個內(nèi)存區(qū)域。

當我們創(chuàng)建 Data 的一個副本的時候,他的所有的字段都會被復(fù)制,但是又不是立刻復(fù)制,因為 Data 內(nèi)存有對實際內(nèi)存空間的引用,所以當結(jié)構(gòu)體被復(fù)制時,也只是會生成一個新的引用,只有我們對這個新的引用修改數(shù)據(jù)是,實際的數(shù)據(jù)才會被復(fù)制。

實現(xiàn)寫時復(fù)制

我們自己實現(xiàn)一個 Data 類型來幫我們理解寫時復(fù)制是如何工作的,我們內(nèi)部使用 NSMutableData 來實際的存儲數(shù)據(jù)(只是為了更快的完成,實際的Data 內(nèi)部肯定是用到更底層的數(shù)據(jù)結(jié)構(gòu)來存儲數(shù)據(jù))。改變數(shù)據(jù)的方法我們只實現(xiàn)一個 append 方法。

struct MyData {
    var data = NSMutableData()
    
    func append(_ bytes: [UInt8]) {
        data.append(bytes, length: bytes.count)
    }
}

我們可以創(chuàng)建一個 MyData

let data = MyData()

為了能更好的打印出 data 中存儲的數(shù)據(jù),我們可以讓 MyData 實現(xiàn) CustomDebugStringConvertible 協(xié)議。

extension MyData: CustomDebugStringConvertible {
    var debugDescription: String {
        return String(describing: data)
    }
}

現(xiàn)在我們可以調(diào)用 append 方法了。

data.append(sampleBytes)

但這是有問題的,首先我們的MyData是結(jié)構(gòu)體,而且創(chuàng)建 data 使用的是let,我們不應(yīng)該可以修改他的值。

而且看下面的代碼,他的復(fù)制行為也是有問題的,在我們聲明了一個新的引用是,并沒有獲得一個完全獨立的副本。

var copy = data
copy.append(sampleBytes)

print(data)
print(copy)
// copy 調(diào)用 append, data 也會改變

所以說我們雖然創(chuàng)建了一個結(jié)構(gòu)體,但是他并沒有表現(xiàn)出值語義來。

目前,我們在把 data 賦給一個新的變量時,雖然他是所有字段都復(fù)制,但是我們MyData內(nèi)部的 data 是一個 NSMutableData 引用類型,所以說 data 和 copy 這兩個變量的值現(xiàn)在都包含對同一個 NSMutableData 實例的引用。

為了解決這個問題,我們要先處理寫時復(fù)制的’寫時‘問題。當我們在調(diào)用 append 方法添加數(shù)據(jù)時,我們要把內(nèi)部進行實際存儲功能的data進行深拷貝,此時 我們的 append 方法就必須加上 mutating 關(guān)鍵字,要不然編譯器不允許修改結(jié)構(gòu)體的變量。

struct MyData {
    var data = NSMutableData()
    
    mutating func append(_ bytes: [UInt8]) {
        print("make a copy")
        data = data.mutableCopy() as! NSMutableData
        data.append(bytes, length: bytes.count)
    }
}

現(xiàn)在我們要重新生成一個 var 類型的 data 來調(diào)用 append 方法,因為編譯器不允許let 類型的調(diào)用帶 mutating 關(guān)鍵字的方法。

var data = MyData()
var copy = data
copy.append(sampleBytes)

在我們繼續(xù)之前,進行一個小的重構(gòu),并將生成 NSMutableData 實例副本的代碼提取到一個單獨的屬性中。

struct MyData {
    var data = NSMutableData()
    var dataForWriting: NSMutableData {
        mutating get {
            print("make a copy")
            data = data.mutableCopy() as! NSMutableData
            return data
        }
    }
    
    mutating func append(_ bytes: [UInt8]) {
        dataForWriting.append(bytes, length: bytes.count)
    }
}

讓寫時復(fù)制更高效

目前我們的寫時復(fù)制是非常簡單的,就是每次當我們調(diào)用 append 的時候,都會拷貝,不管我們是不是這個實例的唯一持有者。

for _ in 0..<10 {
    data.append(sampleBytes)
}
// making a copy 會打印10次

其實真正需要執(zhí)行復(fù)制操作的是當我們把data賦值給另一個變量后,這時調(diào)用append 方法,因為此時有兩個引用,所以需要進行深拷貝。當拷貝結(jié)束后,這兩個都是引用指向的都是完全獨立的備份了,所以再一次調(diào)用時就不需要拷貝了。

所以說我們的MyData結(jié)構(gòu)沒有問題,但是多次拷貝會降低性能。我們可以使用 isKnownUniquelyReferenced 這個方法來幫助我們實現(xiàn)想要的效果。

var dataForWriting: NSMutableData {
    mutating get {
        if isKnownUniquelyReferenced(&data) {
            return data
        }
        print("make a copy")
        data = data.mutableCopy() as! NSMutableData
        return data
    }
}

雖然我們現(xiàn)在加上了 isKnownUniquelyReferenced 檢查,但是運行一下測試代碼還是會copy多次,那是因為 isKnownUniquelyReferenced 方法只是對Swift類型有效果,如果是傳入的OC類型的對象,總是會返回false,所以我們應(yīng)該使用一個Swift類型來包裝一下這個data類型。

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

我們使用這個Box類來包裝 NSMutableData , 最終我們的MyData 變成下面這樣子

struct MyData {
    var data = Box(NSMutableData())
    var dataForWriting: NSMutableData {
        mutating get {
            if isKnownUniquelyReferenced(&data) {
                return data.unbox
            }
            print("make a copy")
            data = Box(data.unbox.mutableCopy() as! NSMutableData)
            return data.unbox
        }
    }
    
    mutating func append(_ bytes: [UInt8]) {
        dataForWriting.append(bytes, length: bytes.count)
    }
}

現(xiàn)在我們的代碼只對 NSMutableData 實例copy一次。

var data = MyData()
var copy = data
for _ in 0..<10 {
    data.append(sampleBytes)
}
// Prints:
// making a copy 一次

標準庫中數(shù)組和字典的實現(xiàn)方式其實也是類似的,只是他們用了更低級的數(shù)據(jù)結(jié)構(gòu)來存儲,我們這樣手動實現(xiàn)一次寫時復(fù)制,有助于我們更好理解他們內(nèi)部的性能。

寫時復(fù)制注意點

寫時復(fù)制很高效,但是他不是適應(yīng)于所有的場景,比如說我們上面的for循環(huán)是可以的,但是如果我們使用reduce來實現(xiàn)上面的循環(huán),他就不起作用了。

(0..<10).reduce(data) { result, _ in
    var copy = result
    copy.append(sampleBytes)
    return copy
}

這個實現(xiàn)方式會生成 10 個副本,因為當我們調(diào)用 append 時,總是有兩個變量——copy 和 result——引用指向同一個實例。

所以我們應(yīng)該注意我們代碼中那些產(chǎn)品大量不必要副本的地方,不過我們一般都不會這么寫,所以說問題不大。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容