Swift: 為什么要避免在結構體中使用閉包?

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

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

原因是:內存泄漏和意外行為

結構體內存泄漏,可能嗎?

結構體是值類型,并且不可能發生內存泄漏。這句話是真的嗎?我們已經有很多問題了。因此,讓我們回顧一下Swift中的內存管理基礎知識。

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

值類型(Value type)

值類型將數據直接存儲在內存中。每個實例都有唯一的數據副本。將變量分配給現有變量后,將復制數據。值類型的分配在堆棧中完成。當值類型變量超出范圍時,將發生內存的重新分配。

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)

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

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變量中的更改。這就是引用類型的工作方式。通常,在引用類型中會發生內存泄漏。在大多數情況下,它以循環引用(retain cycles)的形式出現。
因此,如果引用類型是導致內存泄漏的原因,那么我們可以將值類型用于所有情況。那就應該解決問題。
不幸的是,這種情況并非如此。有時,結構體和枚舉可以被視為引用類型,這意味著循環引用(retain cycles)也可以在結構體和枚舉中發生。

結構體中產生循環引用的罪魁禍首——閉包(Closures)

當您在結構中使用閉包時,閉包的行為就像一個引用類型,問題就從那里開始。閉包需要引用外部環境,以便在執行閉包主體時可以修改外部變量。
在使用類(Class)的情況下,我們可以使用[weak self]打破循環引用。當我們嘗試對某個結構體執行此操作時,會出現以下編譯器錯誤,'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)")
大膽猜測一下最終打印的結果

Swift - Closure - Struct

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

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!

原因解析:

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

這就是為什么Swift結構中的閉包很危險的原因。

直接的解決方案是,避免在值類型中使用閉包。如果要使用它們,則應格外小心,否則可能會導致意外結果。關于保留周期,打破它們的唯一方法是將變量myCarmyNewCar手動設置為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/

賞我一個贊吧~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容