什么时候在Swift中使用强,弱和无主引用类型,为什么
我们已经在Funding Circle使用Swift已有两年了。 值得unowned
一个特定主题是weak
引用类型与unowned
引用类型的用法。 为什么存在三种引用对象的方式( strong
, weak
, unowned
),以及何时应使用每种方式。
首先让我们理解为什么引用计数很重要。 Swift依靠ARC进行内存管理。 顾名思义,它对引用计数,以了解是否必须将对象保留在内存中。 这是一个极其简化的解释,要了解更多信息,请参阅Apple的ARC文档1。
在开始之前,请确保我们的沟通清晰。 在谈论变量时,我们指的是Swift引用类型( class
和function
)。 引用计数对于值类型( struct
, enum
和basic类型)的工作方式不同。 每当更改值类型时,Swift都会在写入时进行复制 。 这意味着,通常,我们可以假定实际上一个值类型的引用计数最多为1。实际上,这些值还指向一个相同的实例,直到更改其中一个值为止,仅在这时将完成原始值的副本,然后进行更改并将其保留在自己分配的内存中。
这是Swift中的默认引用类型。 每当我们声明变量但未指定其引用类型时,该变量将始终为strong
。 strong
引用是指ARC将为变量引用的对象增加引用计数。 这会影响内存管理,因为在引用计数大于零时无法释放对象的内存。
让我们看下面的简化示例:
常量将始终具有强引用类型,因此,当我们声明let balanceConstant
,它将增加内存中Balance
对象的引用计数。 每当指向常量的对象所引用的变量更改时,都会创建该对象的副本,并且该副本就是要更新的副本。
与strong
引用相反, weak
引用对对象的引用计数没有影响。 这意味着,如果我们声明一个指向对象的weak
变量,则该对象的引用计数将与以前相同。 让我们看下面的简单示例,这实际上意味着什么:
我们首先创建一个balance
变量,该变量将对新创建的Balance
对象保持strong
引用。 这将增加对象的引用计数并使它等于1。 接下来,我们声明一个weak
变量balanceCopy
,该变量将不会更改对象的引用计数。 然后,通过将nil
分配给保持对Balance
对象的strong
引用的balance
变量,从对象中删除strong
引用。 这使引用计数为零,从而释放了对象,这意味着我们weak balanceCopy
变量将没有指向的对象,因此当我们尝试对其进行拆包时,结果为nil
。
与weak
引用相似, unowned
引用不会增加对象的引用计数。 但是在用法上有几个重要区别。 weak
和unowned
之间的区别之一是,Swift运行时会为unowned
引用保留辅助引用计数。 当strong
引用计数变为零时,对象将释放它拥有的所有引用,但是当有无用的引用指向该对象时,该对象自己的内存将不会被释放。 但是该对象的内存被标记为zombie
。 这意味着用户不能依赖于该内存中存储的任何内容,并且在没有安全拆包的情况下访问它会导致程序崩溃。 对引用的检查在运行时进行,这就是为什么访问它是运行时错误的原因。 另一个区别是, unowned
变量不能为可选类型。 考虑到Swift会迫使我们使用变量而又不能再次检查它是否指向有效对象,这一点非常重要。 让我们看下面的例子:
首先,我们声明一个可选的balance
变量,该变量持有对Balance
对象的strong
引用。 在下一行中,我们声明一个unowned
变量balanceCopy
,该变量将指向与balance
变量相同的Balance
对象,但不会更改该对象的引用计数。 然后,将nil
分配给balance
变量时, Balance
对象被标记为僵尸,因此无法访问其内存,因此,当我们尝试在balanceCopy上获取金额时,会遇到运行时错误。
鉴于我们已经看到的示例,这一步很简单。 显然,每当我们要保证我们始终能够访问变量时,都应使用strong
引用。 对于对象属性之类的东西,在其所有者的生命周期中应始终存在,尤其如此。 在我们的案例中,要重申一下,当我们有一个贷方对象时,我们知道他肯定会有一个名字并且会有余额,即使他刚用0.00英镑创建了帐户。 有关更多详细示例,请查看Apple的examples1。
当我们开始在对象之间使用双向引用时,情况变得更加复杂。 此类引用在委托模式中很常见,正如我们在以下使用LenderDepositDelegate
定义deposit
操作的示例中看到的LenderDepositDelegate
。
它清楚地描述了一种情况,当使用strong
引用时,两个对象都无法释放,因为它们始终至少有另一个对象指向它们。
因此,在委托模式中,最好不要强烈引用该委托,这是一个好习惯,如下面的代码片段所示。
但是,这是一个非常简单的情况,在现实世界中,分析起来要复杂得多,但这足以理解其背后的一般思想。
正如我们所看到的,就对象的引用计数而言, weak
和unowned
之间没有实际区别。 两者之间的区别在于Swift语言用来处理它们的机制。 当我们使用weak
引用时,Swift显式地使变量成为可选变量,除非我们将其解包,否则我们不允许使用它。 这将创建对引用的强制编译时验证,这意味着如果不先检查它就不允许我们使用它。 这显然可以通过用力解开来克服!
,通常不建议这样做,除非我们能保证该变量指向有效对象。 如果是unowned
引用,则将其隐式展开,这意味着该引用仅在运行时进行验证,如果引用指向已释放的对象,则会导致运行时崩溃。 如果我们不想在程序运行时感到意外,此机制将使weak
引用类型成为安全的选择。
那么我们什么时候应该使用unowned
呢? 如果我们可以保证引用对象的生命周期等于或大于指向它的变量的生命周期,那么这里的规则就是使用它。 在那种情况下,我们可以确定该对象将不会被释放,并且我们可以安全地使用它。 让我们改编前面的示例,以演示unowned
所有权财产的可能用法。
在上面的示例中,可以将余额所有者Lender声明为unowned
资产是安全的,因为我们知道在Balance存在的情况下Lender将始终存在。 如果放款人被重新分配,余额也将被重新分配。 这意味着使用unowned
是安全的,因为放债人在Balance对象的生存期内将始终存在。
尽管unowned
引用相对于weak
引用具有轻微的性能优势,但是对于大多数项目而言,这并不重要。 在实际场景中,很难保证所引用对象的前提在指向它的变量的生命周期中始终可用,除非实体之间的关系能够保证这种情况1。 即使从理论上证明这样的条件很简单,也没有什么可以阻止某人更改该代码,使该条件不再适用,并且我们无意间发生了运行时崩溃。
我们已经看到, strong
, weak
和unowned
都有各自明确的用例。 strong
引用对于声明实体的属性很有用。 weak
基准是打破基准周期的一种安全方法。 至于unowned
,它有一个非常特定的用例,因为当我们知道指针的生命周期至少与指针的生命周期一样长时。 但是,在现实世界中,我们始终面临着更改假设且不再成立的风险,从而将我们曾经安全的代码导致运行时崩溃。 出于这个原因, weak
引用总是比unowned
引用更安全。
- Apple提供了一个指南,其中包含各种参考类型何时生效以及如何中断参考周期的示例1
- 另一个有用的参考是Chris Eidhof,Ole Begemann和AirspeedVelocity撰写的Advanced Swift 4本书
Apple对ARC的工作方式提供了很好的描述,并提供了何时使用每种类型的引用类型的示例方案。 你可以在这里读到它。
最初于 2018年4月27日 发布在 engineering.fundingcircle.com 上。