在 Swift 中,「类」(class
) 类型会被分配在堆 (heap) 中,并使用引用计数来追踪它的生命周期,并在它被销毁的时候从堆中移除。而「结构体」(struct
) 则不需要在堆中分配额外的内存空间,也不使用引用计数器机制,同时也就没有了销毁的步骤。
是吧?
事实上,「堆」、「引用计数」、「清除行为」 这些也适用于「结构体」类型。不过要当心:不适当的行为容易引发问题,接下来我将会向你展示你可能会怎样把「结构体」当成「类」来使用的结果,并告诉你为什么会导致内存泄漏、错误行为和编译器错误。
警告:这篇文章使用了一些 反模式(你千万不要真的去这么干),我这么做是为了突出结构体在使用闭包时一些不容易被注意到的风险,避免危险的最好方式就是掌握好它们,除非你了解风险后还能怡然自得。
目录:
在结构体中类的作用域
虽然一个「结构体」通常不会具有 deinit
方法,但像其它的 Swift 类型一样,他也需要被正确的引用计数。当结构体内的成员变量被引用或者整个结构体被销毁时,都必须正确的将引用计数增加或减少。
事实上我们可以这样做,当一个「结构体」满足一定条件的时候,其引用计数将随「结构体」的相应行为减少,就好像它拥有deinit
方法一样,要做到这一点,我们可以使用 OnDelete 类
public final class OnDelete {
var closure: () -> Void
public init(_ c: () -> Void) {
closure = c
}
deinit {
closure()
}
}
struct DeletionLogger {
let od = OnDelete { print("DeletionLogger deleted") }
}
do {
let dl = DeletionLogger()
print("Not deleted, yet")
withExtendedLifetime(dl) {}
}
当 DeletionLogger
被删除(也就是在 print
之后的 withExtendedLifetime
运行完之后),OnDelete
的闭包将会被执行。
尝试从一个闭包中访问结构体
让我们来正确的初始化一个结构体并且尝试着获取其中一个成员变量:
struct Counter {
let count = 0
var od: OnDelete? = nil
init() {
od = OnDelete { print("Counter value is \(self.count)") }
}
}
"Excellent!",我现在很开心(I'm Angry!)。
struct Counter {
var count: Int
let od: OnDelete
init() {
let c = 0
count = c
od = OnDelete { print("Counter value is \(c)") }
}
}
struct Counter {
var count = 0
let od: OnDelete?
init() {
od = OnDelete { [count] in print("Counter value is \(count)") }
}
}
可是这两个方法并不能真的让我们访问到这个结构体本身。因为这两种方法捕捉到的都只是 count
的不可变副本,但是我们想要得到的是最新的 count
可变值。
struct Counter {
var count = 0
var od: OnDelete?
init() {
od = OnDelete { print("Counter value is \(self.count)") }
}
}
万岁!这样就更完美了。 一切都是可变的并且共享的。 我们捕获到了 count 变量,并且通过了编译。
疯狂的循环
并且这个 OnDelete
的闭包引用了这个 alloc_box
。
闭包引用了这个封装的 Counter
→ 这个封装的 Counter
引用了 OnDelete
→ OnDelete
引用了闭包
当这个循环产生之后,我们的 OnDelete
对象永远都不会被释放,从而也就不会去调用那个闭包。
我们要怎样破解这个循环?
如果 Counter
是一个类,我们可以使用 [weak self]
闭包来避免这个循环强引用,然而 Counter
是一个结构体而不是一个类,试图这样做只会得到一个报错,真糟糕。
我们能不能手动打破这个循环,在构造之后,把 od
属性设置为 nil
?
就像 Joe Groff 在推上说的那样,Swift 发展进程 SE-0035 应该避免此问题的产生,通过限制最大 inout
捕获(也就是Counter.init
方法使用的那种捕捉),直到 @noescape
闭包(这将防止 OnDelete
的尾随闭包被捕获)。
复制行不通,共享引用怎么样?
这样的问题产生是因为我们的方法返回的副本和从 self
的 Counter.init
返回的不同。我们需要的让返回的版本和引用的版本相同。
让我们避免在 init
方法中做任何事情,并且使用一个 static
(静态)方法来替代它。
struct Counter {
var count = 0
var od: OnDelete? = nil
static func construct() -> Counter {
var c = Counter()
c.od = OnDelete{
print("Value loop break is \(c.count)")
}
return c
}
}
do {
var c = Counter.construct()
c.count += 1
c.od = nil
}
还是同样的问题:我们获得了一个 Counter
,它被永久性的嵌入在 OnDelete
上,这不是被返回的那个版本。
struct Counter {
var count = 0
var od: OnDelete? = nil
static func construct() -> () -> () {
var c = Counter()
c.od = OnDelete{
print("Value loop break is \(c.count)")
}
return {
c.count += 1
c.od = nil
}
}
}
do {
var loopBreaker = Counter.construct()
loopBreaker()
}
这样终于奏效了,可以看到我们的 loopBreaker
闭包正确的影响到了 OnDelete
闭包的打印结果。
一些观点
如果你还没有意识到问题的所在...那我们白白浪费了这些时间。
我们一开始只要创建 Counter
为一个「类」,就可以保持分配的堆的数量为 1。
长话短说:如果你需要从一个不同的作用域中访问一个可变的数据,那么结构体很可能不是一个好的选择。
说在最后
别忘了在类的属性中捕获结构体也要考虑循环引用的问题。你不能弱引用得捕获结构体,所以如果发生了一个循环强引用,你需要用其它的方法来打破它。