之前在這篇文章里我們淺析了一下 struct 和 class, 本文章來(lái)源于王巍的 Swift進(jìn)階.
在 Swift 中, 要存儲(chǔ)結(jié)構(gòu)化數(shù)據(jù), 我們可以采用結(jié)構(gòu)體, 枚舉, 類, 使用閉包捕獲變量. 在Swift的標(biāo)準(zhǔn)庫(kù)中, 絕大多數(shù)的公開(kāi)類型是結(jié)構(gòu)體, 枚舉和類只占一小部分.
結(jié)構(gòu)體和類的主要不同點(diǎn):
- 結(jié)構(gòu)體(和枚舉)是值類型, 而類是引用類型, 在設(shè)計(jì)結(jié)構(gòu)體時(shí), 我們可以要求編譯器保證不可變性, 但是對(duì)于類來(lái)說(shuō), 需要程序員自己來(lái)做.
- 內(nèi)存的管理方式不同, 結(jié)構(gòu)體可以被持有及訪問(wèn), 但是類的實(shí)例只能通過(guò)引用來(lái)間接訪問(wèn), 結(jié)構(gòu)體不能被引用, 但是會(huì)被復(fù)制 (結(jié)構(gòu)體的持有者只有一個(gè), 類的實(shí)例可以有多個(gè)持有者)
- 使用類, 我們可以使用繼承來(lái)共享代碼, 而結(jié)構(gòu)體(以及枚舉)是不能被繼承的. 想要在不同結(jié)構(gòu)體或者枚舉之間共享代碼, 我們需要使用組合, 泛型, 協(xié)議擴(kuò)展等技術(shù).
1. 值類型與引用類型
引用類型
軟件中擁有生命周期的對(duì)象非常多, 比如文件句柄任内,通知中心削解,網(wǎng)絡(luò)接口,數(shù)據(jù)庫(kù)連接颠锉,view controller 都是很好的例子, 對(duì)于這些引用類型, 我們可以在初始化和銷毀的時(shí)候進(jìn)行特定的操作, 在對(duì)他們進(jìn)行比較的時(shí)候, 我們是檢查兩者的內(nèi)存地址. 這些類型的實(shí)現(xiàn)都是用了對(duì)象.
值類型
軟件中有許多值類型, URL, 二進(jìn)制數(shù)據(jù), 日期, 錯(cuò)誤, 字符串, 數(shù)字等, 這些類型都可以使用結(jié)構(gòu)體來(lái)實(shí)現(xiàn).
值類型永遠(yuǎn)不會(huì)改變, 它們具有不可變的特性, 在絕大多數(shù)情況下, 這是好事.
- 使用不變的數(shù)據(jù)可以讓代碼容易理解.
- 讓代碼天然具有線程安全的特性 (不能改變的數(shù)據(jù)在線程之間是可以安全共享的)
在Swift中, 結(jié)構(gòu)體可以用來(lái)構(gòu)建值類型的, 結(jié)構(gòu)體不能通過(guò)引用來(lái)比較, 你只能通過(guò)他們的屬性來(lái)比較兩個(gè)結(jié)構(gòu)體.
- 雖然我們可以使用 var 來(lái)在結(jié)構(gòu)體中聲明可變的變量屬性, 但是這個(gè)可變性只體現(xiàn)在變量本身, 而不是里面的值.
- 改變一個(gè)結(jié)構(gòu)體變量的屬性, 在概念上來(lái)說(shuō), 和為整個(gè)變量賦值一個(gè)全新的結(jié)構(gòu)體等價(jià), 我們總是使用一個(gè)新的結(jié)構(gòu)體, 并設(shè)置被改變的屬性值, 然后用它代替原來(lái)的結(jié)構(gòu)體.
struct Size {
var width: Int
var height: Int
}
struct Point {
var x: Int
var y: Int
}
struct Rectangle {
var origin: Point
var size: Size
init(x: Int = 0, y: Int = 0, width: Int, height: Int) {
origin = Point(x: x, y: y)
size = Size(width: width, height: height)
}
}
var screen = Rectangle(width: 100, height: 100) {
didSet {
print(screen)
}
}
screen.origin.x = 10
// 此時(shí)會(huì)觸發(fā) didSet
- 對(duì)結(jié)構(gòu)體進(jìn)行改變,在語(yǔ)義上來(lái)說(shuō)窃页,與重新 為它進(jìn)行賦值是相同的卧蜓。即使在一個(gè)更大的結(jié)構(gòu)體上只有某一個(gè)屬性被改變了,也等同于整個(gè)結(jié)構(gòu)體被用一個(gè)新的值進(jìn)行了替代.
- 在一個(gè)嵌套的結(jié)構(gòu)體的最深層的某個(gè)改變怠李,將會(huì)一路向上反映到最外層的實(shí)例上,并且一路上觸發(fā)所有它遇到的 willSet 和 didSet.
- 雖然我們將整個(gè)結(jié)構(gòu)體替換為了新的結(jié)構(gòu)體蛤克,但是一般來(lái)說(shuō)這不會(huì)損失性能, 編譯器可以原地進(jìn)行變更.
- 由于這個(gè)結(jié)構(gòu)體沒(méi)有其他所有者捺癞,實(shí)際上我們沒(méi)有必要進(jìn)行復(fù)制, 如果有多個(gè)持有者的話, 重新賦值意味著發(fā)生復(fù)制, 對(duì)于寫時(shí)復(fù)制的結(jié)構(gòu)體, 工作方式又有變化.
值語(yǔ)義與引用語(yǔ)義
結(jié)構(gòu)體只有一個(gè)持有者, 比如, 將一個(gè)結(jié)構(gòu)體變量傳遞給一個(gè)函數(shù)時(shí), 函數(shù)將接收到結(jié)構(gòu)體的復(fù)制, 它也只能改變他自己的這份賦值, 這叫做值語(yǔ)義(value semantics), 也被叫做復(fù)制語(yǔ)義.
對(duì)于對(duì)象來(lái)說(shuō), 它是通過(guò)傳遞引用來(lái)工作的, 因此類對(duì)象會(huì)擁有多個(gè)持有者, 這叫做引用語(yǔ)義(reference semantics).
寫時(shí)復(fù)制
值總是需要復(fù)制這件事聽(tīng)起來(lái)可能有點(diǎn)低效, 但是, 編譯器可以幫我們進(jìn)行優(yōu)化. 以避免不必要的復(fù)制操作.
- 結(jié)構(gòu)體復(fù)制的時(shí)候發(fā)生的是按照字節(jié)進(jìn)行的淺復(fù)制.
- 除非結(jié)構(gòu)體中含有類,否則復(fù)制時(shí)都不需要考慮其中屬性的引用計(jì)數(shù).
- 使用 let 來(lái)聲明結(jié)構(gòu)體時(shí)构挤,編譯器可以確定之后這個(gè)結(jié)構(gòu)體的任何一個(gè)字節(jié)都不會(huì)被改變.
- 編譯器還可能使用 傳遞引用 而非 值 的方式來(lái)優(yōu)化一個(gè)常量結(jié)構(gòu)體.
編譯器所做的對(duì)于 值類型的復(fù)制優(yōu)化 和 值語(yǔ)義類型的寫時(shí)復(fù)制 并不是一回事, 寫時(shí)復(fù)制必須由開(kāi)發(fā)者來(lái)實(shí)現(xiàn), 想要實(shí)現(xiàn)寫時(shí)復(fù)制, 你需要檢測(cè)所包含的類是否有共享的引用.
如果一個(gè)結(jié)構(gòu)體只由其他結(jié)構(gòu)體組成髓介,那編譯器可以確保不可變性。同樣地筋现,當(dāng)使用結(jié)構(gòu)體時(shí)唐础,
編譯器也可以生成非常快的代碼, 對(duì)一個(gè)只含有結(jié)構(gòu)體的數(shù)組進(jìn)行操作的效率,通
常要比對(duì)一個(gè)含有對(duì)象的數(shù)組進(jìn)行操作的效率高得多, 這是因?yàn)榻Y(jié)構(gòu)體通常是直接存儲(chǔ)在數(shù)組的內(nèi)存上, 而對(duì)象的數(shù)組中包含的只有對(duì)象的引用, 在很多情況下, 編譯器將結(jié)構(gòu)體放在棧上, 而不放在堆上.
在Swift標(biāo)準(zhǔn)庫(kù)中, 向Array, Dictionary 和 Set 這樣的集合類型是通過(guò)一種叫作寫時(shí)復(fù)制(copy-on-write)的技術(shù)實(shí)現(xiàn)的.
var x = [1,2,3]
var y=x
- 在內(nèi)部, 這些 Array 結(jié)構(gòu)體含有指向某個(gè)內(nèi)存的引用, 這個(gè)內(nèi)存就是數(shù)組中元素所存儲(chǔ)的位置, 兩個(gè)數(shù)組的引用指向的是內(nèi)存中同一個(gè)位置, 這兩個(gè)數(shù)組共享了它們的存儲(chǔ)部分.
- 當(dāng) x 發(fā)生改變時(shí), 這個(gè)共享會(huì)被檢測(cè)到,內(nèi)存將會(huì)被復(fù)制, 這樣一來(lái)森瘪,我們得以獨(dú)立地改變兩個(gè)變量
x.append(5)
y.removeLast()
x // [1, 2, 3, 5]
y // [1, 2]
這種行為就是寫時(shí)復(fù)制, 它的工作方式是
- 每當(dāng)數(shù)組被改變, 它首先檢查它對(duì)存儲(chǔ)緩沖區(qū)的引用是否是唯一的, 或者說(shuō), 檢查數(shù)組本身是不是這塊緩沖區(qū)的唯一擁有者.
- 如果是衫哥,那么緩沖區(qū)可以進(jìn)行原地變更, 此時(shí)不會(huì)有復(fù)制進(jìn)行.
- 如果不是, 如在本例中, 緩沖區(qū)有一個(gè)以上的持有者, 那么數(shù)組就需要先進(jìn)行復(fù)制, 然后對(duì)復(fù)制的值進(jìn)行變化,而保持其他的持有者不受影響.
當(dāng)你自己的類型內(nèi)部含有一個(gè)或多個(gè)可變引用,同時(shí)你想要保持值語(yǔ)義時(shí),你應(yīng)該為其實(shí)現(xiàn)寫時(shí)復(fù)制.
為了維護(hù)值語(yǔ)義,通常都需要進(jìn)行在每次變更時(shí)森篷,都進(jìn)行昂貴的復(fù)制操作,但是寫時(shí)復(fù)制技術(shù)避免了在非必要的情況下的復(fù)制操作.
實(shí)現(xiàn)寫時(shí)復(fù)制
1. 創(chuàng)建一個(gè)不包含值語(yǔ)義的結(jié)構(gòu)體
struct MyData {
var _data: NSMutableData
init(_ data: NSData) {
_data = data.mutableCopy() as! NSMutableData
}
}
extension MyData {
func append(_ byte: UInt8) {
var mutableByte = byte
_data.append(&mutableByte, length: 1)
}
}
let theData = NSData(base64Encoded: "wAEP/w==")!
let x = MyData(theData)
let y = x
x._data === y._data // true
x.append(0x55)
y // <c0010fff 55>
print(y, x) // x, y 的 _data 值是一樣的.
如果我們復(fù)制結(jié)構(gòu)體變量豺型,里面進(jìn)行的是淺復(fù)制. 這意味著對(duì)象本身不會(huì)被復(fù)制, 而只有指向 NSMutableData 對(duì)象的引用會(huì)被復(fù)制.
2. 創(chuàng)建包含值語(yǔ)義的結(jié)構(gòu)體 寫時(shí)復(fù)制(性能較差)
struct MyData {
fileprivate var _data: NSMutableData
fileprivate var _dataForWriting: NSMutableData {
mutating get {
_data = _data.mutableCopy() as! NSMutableData
return _data
}
}
init() {
_data = NSMutableData()
}
init(_ data: NSData) {
_data = data.mutableCopy() as! NSMutableData
}
}
extension MyData {
mutating func append(_ byte: UInt8) {
var mutableByte = byte
_dataForWriting.append(&mutableByte, length: 1)
}
}
let theData = NSData(base64Encoded: "wAEP/w==")! var x = MyData(theData)
lety=x
x._data === y._data // true
x.append(0x55)
y // <c0010fff>
x._data === y._data // false
現(xiàn)在, 這個(gè)結(jié)構(gòu)體具有值語(yǔ)義了, 如果我們將 x 賦值給變量 y, 兩個(gè)變量將繼續(xù)指向底層相同的 NSMutableData 對(duì)象. 不過(guò), 當(dāng)我們對(duì)其中某個(gè)調(diào)用 append 時(shí), 將會(huì)進(jìn)行復(fù)制.
但是, 當(dāng)多次修改同一個(gè)變量時(shí), 這種方式非常浪費(fèi)
var buffer = MyData(NSData())
for byte in 0..<5 as CountableRange<UInt8> {
buffer.append(byte)
}
每次調(diào)用 append 時(shí), 底層的 _data 對(duì)象都要被復(fù)制一次, 因?yàn)?buffer 沒(méi)有和其他的 MyData 實(shí)例共享存儲(chǔ), 解決辦法是對(duì)它進(jìn)行原地變更, 這種方法會(huì)高效得多 (同時(shí)也是安全的)
3. 創(chuàng)建包含值語(yǔ)義的結(jié)構(gòu)體 寫時(shí)復(fù)制(性能較好)
為了提供高效的寫時(shí)復(fù)制特性, 我們需要知道一個(gè)對(duì)象 (比如這里的 NSMutableData) 是否是唯一的. 如果它是唯一引用, 那么我們就可以直接原地修改對(duì)象. 否則, 我們需要在修改前創(chuàng)建對(duì)象的復(fù)制.
在 Swift 中, 我們可以使用 isKnownUniquelyReferenced
函數(shù)來(lái)檢查某個(gè)引用只有一個(gè)持有者, 不過(guò), 對(duì)于 OC 類, 它會(huì)直接返回fasle, 所以我們需要直接創(chuàng)建一個(gè)包裝類.
final class Box<A> {
var unbox: A
init(_ value: A) { self.unbox = value }
}
var x = Box(NSMutableData())
isKnownUniquelyReferenced(&x) // true
如果有多個(gè)引用指向相同的對(duì)象, 這個(gè)函數(shù)將會(huì)返回 false.
vary=x isKnownUniquelyReferenced(&x) // false
struct MyData {
private var _data: Box<NSMutableData>
var _dataForWriting: NSMutableData {
mutating get {
if !isKnownUniquelyReferenced(&_data) {
_data = Box(_data.unbox.mutableCopy() as! NSMutableData)
print("Making a copy")
}
return _data.unbox
}
}
init() {
_data = Box(NSMutableData())
}
init(_ data: NSData) {
_data = Box(data.mutableCopy() as! NSMutableData)
}
}
extension MyData {
mutating func append(_ byte: UInt8) {
var mutableByte = byte
_dataForWriting.append(&mutableByte, length: 1)
}
}
測(cè)試一下代碼
var bytes = MyData()
var copy = bytes
for byte in 0..<5 as CountableRange<UInt8> {
print("Appending 0x\(String(byte, radix: 16))")
bytes.append(byte)
}
/*
Appending 0x0
Making a copy
Appending 0x1
Appending 0x2
Appending 0x3
Appending 0x4
*/
bytes // <00010203 04>
copy // <> 兩個(gè)不共享內(nèi)存, 所以 copy 數(shù)據(jù)是空的
運(yùn)行代碼, 你會(huì)看到上面加入的調(diào)試語(yǔ)句只在第一次調(diào)用 append 的時(shí)候被打印了一次. 在接下來(lái)的循環(huán)中, 引用都是唯一的, 所以也就沒(méi)有進(jìn)行復(fù)制操作.
這項(xiàng)技術(shù)讓你能夠在創(chuàng)建保留值語(yǔ)義的結(jié)構(gòu)體的同時(shí), 保持像對(duì)象和指針那樣的高效操作, 得益于寫時(shí)復(fù)制和與其關(guān)聯(lián)的編譯器優(yōu)化, 大量的不必要的復(fù)制操作都可以被移除掉.
在當(dāng)創(chuàng)建結(jié)構(gòu)體時(shí)仲智,類也還是有其用武之地的.
- 定義一個(gè)只有單個(gè)實(shí)例的從不 會(huì)被改變的類型.
- 封裝一個(gè)引用類型,而并不想要寫時(shí)復(fù)制.
- 需要將接口暴露給 Objective-C
2. 閉包和可變性
var i = 0
func uniqueInteger() -> Int {
i += 1
return I
}
let otherFunc = uniqueInteger
let otherFunc2 = otherFunc
otherFunc() // 1
otherFunc2() // 2
每次我們調(diào)用該函數(shù)時(shí), 共享的變量 i 都會(huì)改變. 如果我們傳遞這些閉包和函數(shù), 它們會(huì)以引用的方式存在, 并共享同樣的狀態(tài).
Swift 的結(jié)構(gòu)體一般被存儲(chǔ)在棧上, 而非堆上. 不過(guò)對(duì)于可變結(jié)構(gòu)體, 這其實(shí)是一種編譯器優(yōu)化:
- 默認(rèn)情況下結(jié)構(gòu)體是存儲(chǔ)在堆上的, 但是在絕大多數(shù)時(shí)候, 編譯器會(huì)優(yōu)化內(nèi)存, 將結(jié)構(gòu)體存儲(chǔ)到棧上.
- 編譯器這么做是因?yàn)槟切┍惶右蓍]包捕獲的變量需要在棧幀之外依然存在.
- 當(dāng)編譯器偵測(cè)到結(jié)構(gòu)體變量被一個(gè)函數(shù)閉合的時(shí)候, 優(yōu)化將不再生效, 此時(shí)這個(gè)結(jié)構(gòu)體將存儲(chǔ)在堆上. 這樣一來(lái), 就算
uniqueIntegerProvider
退出了作用域, i 也將繼續(xù)存在.
func uniqueIntegerProvider() -> () -> Int {
var i = 0
return {
I+=1
return I
}
}
3. 內(nèi)存
在標(biāo)準(zhǔn)庫(kù)中大部分的類型是 結(jié)構(gòu)體 或者 枚舉, 他們是值類型, 它們只會(huì)有一個(gè)持有者, 所以它們所需要的內(nèi)存可以被自動(dòng)地創(chuàng)建和釋放. 當(dāng)使用值類型時(shí), 不會(huì)產(chǎn)生循環(huán)引用的問(wèn)題.
struct Person {
let name: String
var parents: [Person]
}
var john = Person(name: "John", parents: [])
john.parents = [John]
john//John,parents:[John,parents:[]]
因?yàn)橹殿愋偷奶攸c(diǎn)姻氨,當(dāng)你把 john 加到數(shù)組中的時(shí)候钓辆,其實(shí)它被復(fù)制了, john 的值被加入到數(shù)組中, 如果 Person 是一個(gè)類的話,那么必然會(huì)造成循環(huán)引用.
對(duì)于類,Swift 使用自動(dòng)引用計(jì)數(shù) (ARC) 來(lái)進(jìn)行內(nèi)存管理, 每次你創(chuàng)建一個(gè)對(duì)象的新的引用 (比如為類變量賦值), 引用計(jì)數(shù)會(huì)被加一, 一旦引用失效(比如變量離開(kāi)了作用域), 引用計(jì)數(shù)將被減一, 如果引用計(jì)數(shù)為零, 對(duì)象將被銷毀. 遵循這種行為模式的變量也被叫做 強(qiáng)引用
循環(huán)引用
class View {
var window: Window
init(window: Window) {
self.window = window
}
}
class Window {
var rootView: View?
}
var window: Window? = Window() // window: 1
var view: View? = View(window: window!) // window: 2, view: 1 window?.rootView = view // window: 2, view: 2
view = nil // window: 2, view: 1
window = nil // window: 1, view: 1
- 我們創(chuàng)建了 window 對(duì)象, window 的引用計(jì)數(shù)將為 1.
- 之后創(chuàng)建 view 對(duì)象時(shí), 它持有了 window 對(duì)象的強(qiáng)引用前联,所以這時(shí)候 window 的引用計(jì)數(shù)為 2, view 的計(jì)數(shù)為 1.
- 接下來(lái), 將 view 設(shè)置為 window 的 rootView 將會(huì)使 view 的引用計(jì)數(shù)加一. 此時(shí) view 和 window 的引 用計(jì)數(shù)都是 2.
- 當(dāng)把兩個(gè)變量都設(shè)置為 nil 后功戚,它們的引用計(jì)數(shù)都會(huì)是 1.
即使它們已經(jīng)不能通過(guò)變量進(jìn)行訪問(wèn)了, 但是它們卻互相有著對(duì)彼此的強(qiáng)引用. 這就被叫做引用循環(huán) . 因?yàn)榇嬖谝醚h(huán), 這樣的兩個(gè)對(duì)象在程序
的生命周期中將永遠(yuǎn)無(wú)法被釋放.
要打破循環(huán), 需要確保其中一個(gè)引用要么是 weak, 要么是 unowne.
weak引用
- 當(dāng)你將一個(gè)變量標(biāo)記為 weak 時(shí), 將某個(gè)值賦值給這個(gè)變量時(shí), 它的引用計(jì)數(shù)不會(huì)被改變.
- Swift 中的弱引用 是趨零的: 當(dāng)一個(gè)弱引用變量所引用的對(duì)象被釋放時(shí), 這個(gè)變量將被自動(dòng)設(shè)為 nil。這也是弱引用必須被聲明為可選值的原因.
class View {
var window: Window
init(window: Window) {
self.window = window
}
deinit {
print("Deinit View")
}
}
class Window {
weak var rootView: View?
deinit {
print("Deinit Window")
}
}
var window: Window? = Window()
var view: View? = View(window: window!)
window?.rootView = view
view = nil
window = nil
/*
Deinit Window
Deinit View
*/
unowned引用
因?yàn)?weak 引用的變量可以變?yōu)?nil, 所以它們必須是可選值類型. 但是, 如果我們知道我們的 view 將一定有一個(gè) window, 這樣這個(gè)屬性就不應(yīng)該是可選值似嗤,而同時(shí)我們又不想一個(gè) view 強(qiáng)引用 window. 這種情況下, 我們可以使用 unowned 關(guān)鍵字.
class View {
unowned var window: Window
init(window: Window) {
self.window = window
}
deinit {
print("Deinit View")
}
}
class Window {
var rootView: View?
deinit {
print("Deinit Window")
}
}
var window: Window? = Window()
var view: View? = View(window: window!)
window?.rootView = view
view = nil
window = nil
/*
Deinit Window
Deinit View
*/
這樣寫依然沒(méi)有引用循環(huán), 但是我們要負(fù)責(zé)保證 window 的生命周期比 view ?. 如果 window 先被銷毀, 然后我們?cè)L問(wèn)了 view 上這個(gè) unowned 的變量的話, 就會(huì)造成運(yùn)行崩潰.
對(duì)每個(gè) unowned 的引用, Swift 運(yùn)行時(shí)將為這個(gè)對(duì)象維護(hù)另外一個(gè)引用計(jì)數(shù).
- 當(dāng)所有的 strong 引用消失時(shí), 對(duì)象將把它的資源 (比如對(duì)其他對(duì)象的引用) 釋放掉.
- 不過(guò), 這個(gè)對(duì)象本身的內(nèi)存將繼續(xù)存在, 直到所有的 unowned 引用也都消失.
- 這部分內(nèi)存將被標(biāo)記為無(wú)效 (有時(shí)候我們也 把它叫做僵尸 (zombie) 內(nèi)存), 當(dāng)我們?cè)噲D訪問(wèn)這樣的 unowned 引用時(shí), 就會(huì)發(fā)生運(yùn)行時(shí)錯(cuò)誤.
除了 weak, unowned, 我們還可以選擇 unowned(unsafe), 它不會(huì)做運(yùn)行時(shí)的檢查. 當(dāng)我們?cè)L問(wèn)一個(gè)已經(jīng)無(wú)效的 unowned(unsafe) 引用時(shí), 這時(shí)候結(jié)果將是未定義的.
在 unowned 和 weak 之間進(jìn)行選擇
- 如果這些對(duì)象的生命周期互不相關(guān), 也就是說(shuō), 你不能保證哪一個(gè)對(duì)象存在的時(shí)間會(huì)比另一個(gè)?, 那么 weak 就是唯一的選擇.
- 如果你可以保證非強(qiáng)引用對(duì)象擁有和強(qiáng)引用對(duì)象同樣或者更?的生命周期的話, unowned 引用通常會(huì)更方便一些. 因?yàn)槲覀兛梢圆恍枰幚砜蛇x值, 而且變量將可以被
let 聲明, 而與之相對(duì), 弱引用必須被聲明為可選的 var.
4. 閉包和內(nèi)存
在 Swift 中, 除了類以外, 函數(shù) (包括閉包) 也是引用類型. 閉包可以捕獲變量, 如果這些變量自身是引用類型的話, 閉包將持有對(duì)它們的強(qiáng)引用.
閉包捕獲它們的變量的一個(gè)問(wèn)題是它可能會(huì) (意外地) 引入引用循環(huán).
常?的模式是這樣的: 對(duì) 象 A 引用了對(duì)象 B, 但是對(duì)象 B 存儲(chǔ)了一個(gè)包含對(duì)象 A 的回調(diào).
class View {
var window: Window
init(window: Window) {
self.window = window
}
deinit {
print("Deinit View")
}
}
class Window {
weak var rootView: View?
var onRotate: (() -> ())?
deinit {
print("Deinit Window")
}
}
var window: Window? = Window()
var view: View? = View(window: window!)
window?.rootView = view
view 強(qiáng)引用了 window, 但是 window 只是弱引用 view, 一切安好.
window?.onRotate = {
print("We now also need to update the view: \(view)")
}
但是, 回調(diào)引用 view, 那么循環(huán)產(chǎn)生.
打破循環(huán)的方式
- 讓指向 window 的引用變?yōu)?weak, 不過(guò), 這會(huì)導(dǎo)致 window 消失, 因?yàn)闆](méi)有其他指向它的強(qiáng)引用了.
- 讓 window 的 onRotate 閉包聲明為 weak, 不過(guò) Swift 不允許將閉包標(biāo)記為 weak.
- 通過(guò)使用捕獲列表(capturelist)來(lái)讓閉包不去引用視圖. 正解.
捕獲列表
window?.onRotate = { [weak view] in
print("We now also need to update the view: \(view)")
}
捕獲列表也可以用來(lái)初始化新的變量, 甚至可以定義完全不相關(guān)的變量. 不過(guò)這些變量的作用域只在閉包內(nèi)部, 在閉包外是不能使用的.
window?.onRotate = { [weak view, weak myWindow = window, x=25] in
print("We now also need to update the view: \(view)")
print("Because the window \(myWindow) changed")
}
總結(jié):
我們研究了 Swift 中結(jié)構(gòu)體和類的種種不同.
- 對(duì)于需要同一性保證的實(shí)體, 類會(huì)是更好的選擇. 而對(duì)于值類型, 結(jié)構(gòu)體會(huì)更好.
- 當(dāng)我們想要在結(jié)構(gòu)體中包含對(duì)象時(shí), 我們往往需要像是寫時(shí)復(fù)制這樣的額外步驟, 來(lái)確保這個(gè)值保持值語(yǔ)義.
- 我們還討論了在處理類時(shí), 要如何避免引用循環(huán)的問(wèn)題.
- 通常來(lái)說(shuō), 一個(gè)問(wèn)題既可以用結(jié)構(gòu)體解決, 也可以用類解決. 具體使用哪個(gè), 要根據(jù)你的需求來(lái)決定. 不過(guò), 就算是那些一般來(lái)說(shuō)會(huì)使用引用來(lái)解決的問(wèn)題, 也可能可以從使用值類型中受益.