Swift: 為什么要避免在結(jié)構(gòu)體中使用閉包?

我們都喜歡閉包,不是嗎?

閉包可以簡化iOS開發(fā)人員的工作。好吧,如果這使我們工作變得容易,那為什么我要避免在Swift結(jié)構(gòu)體中使用閉包呢?

原因是:內(nèi)存泄漏和意外行為。

結(jié)構(gòu)體內(nèi)存泄漏,可能嗎?

結(jié)構(gòu)體是值類型,并且不可能發(fā)生內(nèi)存泄漏。這句話是真的嗎?我們已經(jīng)有很多問題了。因此,讓我們回顧一下Swift中的內(nèi)存管理基礎(chǔ)知識。

Swift中的基本類型分為兩類。一種是“引用類型(Reference type)”,另一種是“值類型(Value type)”。通常,類是引用類型。另一方面,結(jié)構(gòu)體和枚舉是值類型。

值類型(Value type)

值類型將數(shù)據(jù)直接存儲在內(nèi)存中。每個實例都有唯一的數(shù)據(jù)副本。將變量分配給現(xiàn)有變量后,將復(fù)制數(shù)據(jù)。值類型的分配在堆棧中完成。當(dāng)值類型變量超出范圍時,將發(fā)生內(nèi)存的重新分配。

struct Person {
    var name : String
}
var oldPerson = Person(name: "韋弦zhy")
var newPerson = oldPerson
newPerson.name = "Swift Struct"
print(oldPerson.name)
print(newPerson.name)

-------
Output:
韋弦zhy
Swift Struct
-------

我們可以看到,更改newPerson的值不會更改oldPerson的值。這就是值類型的工作方式。

引用類型(Reference type)

引用類型在初始化時保留對數(shù)據(jù)的引用(即指針)。只要將變量分配給現(xiàn)有引用類型,該引用就在變量之間共享。引用類型的分配在堆中完成。ARC(自動引用計數(shù))處理引用類型變量的取消分配。

class Person {
    var name: String
    init(withName name: String){
        self.name = name
    }
}
var oldPerson = Person(withName: "韋弦zhy")
var newPerson = oldPerson
newPerson.name = "Swift Struct"
print(oldPerson.name)
print(newPerson.name)


------
Output
Swift Struct
Swift Struct
------

我們可以看到更改oldPerson變量反映了newPerson變量中的更改。這就是引用類型的工作方式。通常,在引用類型中會發(fā)生內(nèi)存泄漏。在大多數(shù)情況下,它以循環(huán)引用(retain cycles)的形式出現(xiàn)。
因此,如果引用類型是導(dǎo)致內(nèi)存泄漏的原因,那么我們可以將值類型用于所有情況。那就應(yīng)該解決問題。
不幸的是,這種情況并非如此。有時,結(jié)構(gòu)體和枚舉可以被視為引用類型,這意味著循環(huán)引用(retain cycles)也可以在結(jié)構(gòu)體和枚舉中發(fā)生。

結(jié)構(gòu)體中產(chǎn)生循環(huán)引用的罪魁禍首——閉包(Closures)

當(dāng)您在結(jié)構(gòu)中使用閉包時,閉包的行為就像一個引用類型,問題就從那里開始。閉包需要引用外部環(huán)境,以便在執(zhí)行閉包主體時可以修改外部變量。
在使用類(Class)的情況下,我們可以使用[weak self]打破循環(huán)引用。當(dāng)我們嘗試對某個結(jié)構(gòu)體執(zhí)行此操作時,會出現(xiàn)以下編譯器錯誤,'weak' may only be applied to class and class-bound protocol types, not 'struct name',比如如下代碼:

struct Car {
    var speed: Float = 0.0
    var increaseSpeed: (() -> ())?
}
var myCar = Car()
myCar.increaseSpeed = { //[weak myCar] in
    myCar.speed += 30
    // The retain cycle occurs here. We cannot use [weak myCar] as myCar is a value type.
    //'weak' may only be applied to class and class-bound protocol types, not 'Car'
}
myCar.increaseSpeed?()
print("1: My car's speed \n\(myCar.speed)")

var myNewCar = myCar
print("2: My new car's speed \n\(myNewCar.speed)")

myNewCar.increaseSpeed?()
print("3: My new car's speed \n\(myNewCar.speed)")

myCar.increaseSpeed?()
print("4: My car's speed \n\(myCar.speed)")
大膽猜測一下最終打印的結(jié)果

Swift - Closure - Struct

我想你開始想的是3和4最終打印的速度值都是——60,但是結(jié)果可能有點不一樣:

1: My car's speed 
30.0
2: My new car's speed 
30.0
3: My new car's speed 
30.0
4: My car's speed 
90.0

是的,是90!

原因解析:

結(jié)構(gòu)體myNewCar是結(jié)構(gòu)體myCar的部分副本。由于閉包及其環(huán)境無法完全復(fù)制,屬性speed的值被復(fù)制了,但是myNewCar的屬性increaseSpeed在捕獲的環(huán)境變量中引用了myCarincreaseSpeedmyCarspeed。因此,myNewCar.increaseSpeed?()最終調(diào)用的是myCarincreaseSpeed,所以最終打印的值就是myCar的值變成了90。

這就是為什么Swift結(jié)構(gòu)中的閉包很危險的原因。

直接的解決方案是,避免在值類型中使用閉包。如果要使用它們,則應(yīng)格外小心,否則可能會導(dǎo)致意外結(jié)果。關(guān)于保留周期,打破它們的唯一方法是將變量myCarmyNewCar手動設(shè)置為nil。聽起來并不理想,但是沒有其他方法。

參考:

[1] https://ohmyswift.com/blog/2020/01/10/why-should-we-avoid-using-closures-in-swift-structs/
[2] https://github.com/Wolox/ios-style-guide/blob/master/rules/avoid-struct-closure-self.md
[3] https://www.objc.io/issues/16-swift/swift-classes-vs-structs/
[4] https://marcosantadev.com/capturing-values-swift-closures/

賞我一個贊吧~~~

最后編輯于
?著作權(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ù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

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

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