Swift委托 - 在什么情况下使用委托的弱指针?

48

有人能解释一下在Swift中何时使用和不使用弱代理指针赋值,以及为什么吗?

我理解的是,如果您使用未定义为类的协议,则不能也不想将委托指针分配为弱引用。

protocol MyStructProtocol{
    //whatever
}

struct MyStruct {
    var delegate: MyStructProtocol?
}

然而,当您的协议定义为类类型协议时,您确实希望将代理设置为弱指针?

protocol MyClassProtocol: class{
    //whatever
}

class MyClass {
    weak var delegate: MyClassProtocol?
}

我是正确的吗?在苹果的 Swift 指南中,类协议示例没有使用弱引用,但在我的测试中,如果我的委托没有被弱引用,则会出现强引用循环。


这似乎很相关:http://blog.xebia.com/2014/10/09/function-references-in-swift-and-retain-cycles/ - Robert Harvey
如果你将协议声明为 protocol MyStructProtocol : class { ... },那么你可以将代理设为 weak。请参见 https://dev59.com/aGAf5IYBdhLWcg3w_W9j#24104371。 - Rob
@Rob这是否意味着,如果我没有将我的协议声明为类,那么我的委托指针会导致保留循环? - nwales
4
如果没有将代理设为“弱引用”,并不总是会导致强引用循环,而只是增加了这种可能性。 - Rob
3个回答

55

通常情况下,把类的协议声明为 weak 是为了避免"强引用循环"(以前称为"保留循环")的风险。(请注意,现在我们通过将 AnyObject 协议添加到协议的继承列表中来实现;参见仅限类的协议;我们不再使用 class 关键字。)未将委托声明为 weak 并不意味着你本质上会有一个强引用循环,只是说你可能会有这样一个问题。

但对于 struct 类型,由于它们不是"引用"类型,因此强引用循环的风险大大降低。但如果委托对象是一个类对象,那么你可能需要将协议声明为类协议并使其成为 weak。

在我看来,将类委托声明为 weak 只是部分地减轻了强引用循环的风险,也涉及到所有权的问题。大多数委托协议都是对象没有权利要求拥有委托,而只是提供了向委托通知某事情(或请求其完成某事)的能力。例如,如果你想让视图控制器具有一些文本字段委托方法,那么文本字段就没有权利要求拥有视图控制器。


1
很想看到一个结构体协议创建强引用循环的例子,以及何时不会创建,但这个答案已经帮我澄清了很多事情。 - nwales
在您的gist中,对于非强引用循环的示例,如果将MyStructDelegate分配给:class关键字,但MyStruct是一个结构体,则会出现EXC_BAD_ACCESS错误。如果将MyStruct分配为类,则错误消失。这是一个笔误吗?如果是这样,那么我认为只有当委托是类时,才能使用:class协议,但指向委托的对象也必须是类? - nwales
2
当然。在对象层次结构中,子对象不应该保持对父对象的强引用。这是一个红旗,表明存在强引用循环。请注意,在此VC示例中,强引用循环并不总是会表现为泄漏,但在特殊情况下可能会出现问题,因此最好通过将委托设置为弱属性来避免潜在问题。 - Rob
@Rob 太棒了,谢谢。您是知识的源泉,先生 :) 如果我的委托导致内存泄漏,那么拥有一个强大的委托是否与此问题有关 https://stackoverflow.com/questions/48663577/exc-breakpoint-when-not-forcefully-unwrapping? - user2363025
1
@nwales,根据Rob的评论,我写了一个更加清晰的例子在这里,说明缺少weak不会创建强引用循环的情况。 - mfaani
显示剩余6条评论

13

正如Rob所说:

这实际上是一个“所有权”的问题

这非常正确。"强引用循环"关乎获取正确的所有权。

在以下示例中,我们没有使用weak var。但两个对象都将被解除分配。为什么?

protocol UserViewDelegate: class {
    func userDidTap()
}

class Container {
    let userView = UserView()
    let delegate = Delegate()
    init() {
        userView.delegate = delegate
    }

    deinit {
        print("container deallocated")
    }
}

class UserView {
    var delegate: UserViewDelegate?

    func mockDelegatecall() {
        delegate?.userDidTap()
    }

    deinit {
        print("UserView deallocated")
    }
}

class Delegate: UserViewDelegate {
    func userDidTap() {
        print("userDidTap Delegate callback in separate delegate object")
    }
}

用法:

var container: Container? = Container()
container?.userView.mockDelegatecall()
container = nil // will deallocate both objects

内存所有权图(不含循环)

    +---------+container +--------+
    |                             |
    |                             |
    |                             |
    |                             |
    |                             |
    |                             |
    v                             v
userView +------------------> delegate

为了创建一个强引用循环,循环需要是完整的。delegate需要指向container,但它没有。所以这不是一个问题。但纯粹基于所有权原因,正如Rob在这里所说:
在对象层次结构中,子对象不应该维护对父对象的强引用。这是一个红旗,表明存在强引用循环。
所以无论是否泄漏,仍然要为您的delegate对象使用weak。
在下面的示例中,我们没有使用weak var。因此,这两个类都不会被解除分配。
protocol UserViewDelegate: class {
    func userDidTap()
}

class Container: UserViewDelegate {
    let userView = UserView()

    init() {
        userView.delegate = self
    }

    func userDidTap() {
        print("userDidTap Delegate callback by Container itself")
    }
    deinit {
        print("container deallocated")
    }
}

class UserView {
    var delegate: UserViewDelegate?

    func mockDelegatecall() {
        delegate?.userDidTap()
    }

    deinit {
        print("UserView deallocated")
    }
}

用法:

var container: Container? = Container()
container?.userView.mockDelegatecall()
container = nil // will NOT deallocate either objects

内存所有权图(存在循环)

     +--------------------------------------------------+
     |                                                  |
     |                                                  |
     +                                                  v
 container                                           userview
     ^                                                  |
     |                                                  |
     |                                                  |
     +------+userView.delegate = self //container+------+

使用 weak var 可以避免强引用循环


9

代理应该一般使用弱引用。

假设ba的代理。现在,adelegate属性是b

当您希望c消失时,b也释放掉

如果cb有强引用,并且c被释放,您希望bc一起释放。然而,如果在a中使用一个强引用来持有代理b,那么b永远不会被释放,因为a强引用了它。相反,如果使用弱引用,一旦b失去了来自c的强引用,b会在c释放时释放。

通常这就是期望的行为,这就是为什么您需要使用weak属性。


3
我仍然感到困惑。如果我不能将"weak"关键字分配给非类类型的协议,那是否意味着这会导致保留循环?何时使用类协议和非类协议?如果我使用一个结构体,那么我只能使用非类协议,还是必须要与一个类一起使用类协议? - nwales
1
@nwales 我知道这是一条旧评论,但如果你同时使用引用(class)类型并且需要一个weak引用,那么你可以使用class协议。否则,将其声明为class协议是不必要的。只有在你的协议需要时才指定class(例如它是一个委托协议)。因此,如果你正在使用struct(值类型)或者你正在使用class但不需要担心强引用循环(例如用于定义非委托接口的协议),那么就不要将其作为class协议。 - Rob
4
值得一提的是,说委托对象应该“总是”使用弱引用有点过分。以 URLSession 为例,它在会话失效之前一直对其 delegate 使用强引用。然而这种做法只有在会话失效时手动解除强引用才能正常工作。但通常情况下,使用弱引用作为委托对象属性是正确的惯例。+1 - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接