第五章——結構體與類(可變性)

bestswifter發表於2017-12-27

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

在多執行緒程式設計時,很容易遇到一些bug,而且這些bug往往不會直觀地表現出來,從而給bug復現、除錯帶來了一些困難。多執行緒中的bug,很多情況下是由於一個執行緒修改了某個值,而另一個執行緒讀取到的還是舊的值導致的。因此,越來越多的專家認為不可變的變數以及值型別有助於在多執行緒環境中保證程式更加不容易出錯。

舉個例子,我們可以用不可變的NSData替換NSMutableData

func processData(x: NSData) {
// 處理x
}
複製程式碼

但是這樣的函式定義並不能保證x就不可變了,因為NSMutableDataNSData的子類,所以一個NSMutableData物件也可以作為引數傳入processData函式中,這時x就是一個可變的物件了。

一個解決方案是在processData函式的一開始,就把x複製一份,然後副本進行處理:

func processData(x: NSData) {
let data = x.copy() as! NSData
// 處理data
}
複製程式碼

這時,我們確保了data是不可變的。但這麼做有兩個缺點:

  1. 如果引數本身就是不可變的,或者可變引數實際上沒有發生改變,那這樣的複製是毫無意義的行為。
  2. 每次都要複製很麻煩,也很容易忘記。

定義為let的物件不是真正不可變的。let僅僅表示它不能變為其他物件,但是並不保證它內部的屬性不發生變化。而定義為let的結構體是真正不可變的。

值型別

值語義表示,當變數被複制時,複製的是這個變數自己,而不是它的引用。在Swift中,結構體和列舉具有值語義,而物件總是按引用傳遞。儘管結構體的複製看上去有些浪費,但是編譯器會自動為此進行優化。以實現高斯模糊的結構體為例:

struct GaussianBlur {
var input: CIImage
var radius: Double
}

var blur1 = GaussianBlur(input: image, radius: 10)
var blur2 = blur1
blur2.radius = 20	// 如果沒有這一行,其實Swift不會複製結構體blur1
複製程式碼

只有在blur2radious屬性發生了變化時,才會真正複製blur1。這就是“寫時複製”。在複製時,input的引用被複制,所以兩個結構體其實共享了同一個CIImage物件。不過CIImage不是可變的,所以這樣做沒有關係。

為了返回模糊後的圖片,我們改寫一下結構體:

struct GaussianBlur {
private var filter: CIFilter

init(inputImage: CIImage, radious: Double) {
filter = CIFilter(name: "CIGaussianBlur",
withInputParameters:[
kCIInputImageKey: inputImage,
kCIInputRadiousKey: radious
])!
}
}

extension GaussianBlur {
var input: CIImage {
get { return filter.valueForKey(kCIInputImageKey) as! CIImage }
set { filter.setValue(newValue, forKey: kCIInputImageKey) }
}

var radius: Double {
get { return filter.valueForKey(kCIInputRadiousKey) as! Double}
set { filter.setValue(newValue, forKey: kCIInputRadiousKey) }
}
}

extension GaussianBlur {
var outputImage: CIImage {
return filter.outputImage!
}
}
複製程式碼

用法與剛剛類似:

var blur = GaussianBlur(input: image, radius: 10)
blur.outputImage
複製程式碼

但是這種寫法是有問題的,因為結構體複製時會複製所有成員,而filter作為一個CIFilter型別的成員,複製時只會複製引用而不是真正的物件。所以如果我們試圖複製GaussianBlur結構體:

var otherBlur = blur
otherBlur.radius = 20
複製程式碼

就會導致兩個結構的radius都變成了20。

寫時複製

值語義有利有弊。一方面,它在底層使用物件或C指標,從而允許我們建立值型別的結構體,另一方面它可能導致物件被共享。接下來我們一起學習如何用寫時複製技術避免這種物件共享。

Swift中有一個函式叫isUniquelyReferencedNonObjC,它可以判斷一個指標是否被唯一引用。有了這個方法,我們只要複製被多引用的物件,這樣就避免了不必要的複製。悲催的是,正如這個方法名字所寫,它不適用於OC的物件,只有Swift中的物件可以用它。所以,我們先要建立一個簡單的包裝型別Box,它內部可以包裝任何型別的變數:

final class Box<A> {
var unbox: A
init(_ value: A) { unbox = value }
}
複製程式碼

定義好這個方法之後,在結構體中儲存的成員就不是CIFilter本身,而是包裝了CIFilterBox了:

private var boxedFilter: Box<CIFilter> = {
var filter = CIFilter(name: "CIGaussianBlur", withInputParameters:[:])!
filter.setDefaults()
return Box(filter)
}()

var filter: CIFilter {
get { return boxedFilter.unbox}
set { boxedFilter = Box(newValue)}
}
複製程式碼

然後,我們定義一個私有的成員filterForWriting,它只有在需要時才會複製:

private var filterForWriting: CIFilter {
mutating get {
if !isUniquelyReferencedNonObjC(&boxedFilter) {
filter = filter.copy() as! CIFilter
}
return filter
}
}

// Set方法中的是filterForWriting
var input: CIImage {
get { return filter.valueForKey(kCIInputImageKey) as! CIImage }
set { filterForWriting.setValue(newValue, forKey: kCIInputImageKey) }
}

var radius: Double {
get { return filter.valueForKey(kCIInputRadiousKey) as! Double}
set { filterForWriting.setValue(newValue, forKey: kCIInputRadiousKey) }
}
複製程式碼

這樣,如果結構體的inputradius屬性被修改,它在內部就會複製一份filter,也就不會影響到別的結構體了。其實這也是Swift實現陣列寫時複製的方法。有興趣的讀者可以通過===運算子來驗證寫時複製是否被正確的實現了。

在建立自定義的結構體和類的時候,要考慮它們的可變性,以及值語義。在結構體內部使用類時,要確保它真的是不可變的。如果做不到,就應該考慮使用類而不是結構體。Swift中大多數資料結構都是值型別,而OC的Foundation庫中的NSArrayNSString等則需要我們自己手動管理拷貝問題。

閉包與可變性

之前我們一直強調,變數有三種方式儲存:結構體、列舉、類。其實這並不完全,除此之外還有第四種:閉包。閉包和類一樣,都是引用型別。比如我們在第二章——集合協議中說到的生成器,它是引用型別,所以把一個生成器賦值給另一個生成器並不意味著可以遍歷集合兩次,這就是為什麼需要SequenceType來封裝生成器物件的建立過程。

如果要得到多個互不相關的閉包,我們可以這樣寫:

func uniqueIntegerProvider() -> () -> Int {
var i = 0
return { ++i }
}
複製程式碼

這裡定義了一個函式uniqueIntegerProvider,它沒有引數,返回值為() -> Int型別的閉包。每次呼叫這個函式都可以獲得一個新的閉包。

相關文章