囚犯约瑟夫·赖特 -强大的俘虏插图
“已捕获”值的列表位于闭包参数列表的前面,可以通过三种不同方式从范围“捕获”值:使用链接“强”,“弱”或“无主”。 我们经常使用它,主要是为了避免强参考循环(“强参考循环”或“保留循环”)。
新手开发人员可能很难决定采用哪种方法,因此您可以花很多时间在“强”和“弱”之间或“弱”和“无主”之间进行选择,但是随着时间的推移,您将意识到正确的选择-只有一个。
首先,创建一个简单的类:
class Singer { func playSong() { print("Shake it off!") } }
然后,我们编写一个函数,该函数创建
Singer类的实例并返回一个闭包,该闭包调用
Singer类的
playSong()方法:
func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing }
最后,我们可以在任何地方调用
sing()以获得
播放playSong()的结果
let singFunction = sing() singFunction()
结果,将显示“ Shake it off!”行。
强力捕捉
除非您明确指定捕获方法,否则Swift将使用“强”捕获。 这意味着闭包捕获使用的外部值,并且永远不会释放它们。
让我们再次看一下
sing()函数
func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing }
taylor常量是在函数内部定义的,因此在正常情况下,一旦函数完成工作,就可以释放其位置。 但是,此常量在闭包内部使用,这意味着只要闭包本身存在,即使在函数结束之后,Swift也会自动确保其存在。
这是一个“强大”的动作。 如果Swift允许释放
taylor ,则调用闭包将是不安全的-其
taylor.playSong()方法不再有效。
“弱”捕获(弱捕获)
Swift允许我们创建一个“
捕获列表 ”来确定如何捕获所使用的值。 “强”捕获的替代方法是“弱”捕获,其应用会导致以下后果:
1.闭包不保留“弱”捕获值,因此可以释放它们并将其设置为
nil 。
2.作为第一段的结果,Swift中“弱”捕获的值始终是
可选的 。
我们使用“弱”捕获来修改示例,然后立即看到差异。
func sing() -> () -> Void { let taylor = Singer() let singing = { [weak taylor] in taylor?.playSong() return } return singing }
[弱泰勒] -这是我们的“
捕获列表 ”,它是闭包语法的特殊部分,其中我们给出了有关如何捕获值的说明。 这里我们说
taylor应该被“弱”地捕获,所以我们需要使用
taylor?.PlaySong() -现在它是
可选的 ,因为可以随时将其设置为
nil 。
如果现在执行此代码,您将看到调用
singFunction()不再产生消息。 这是因为
taylor仅存在于
sing()内部,并且此函数返回的闭包并未将
taylor牢固地保持在其内部。
现在尝试将
taylor?.PlaySong()更改为
taylor!.PlaySong() 。 这将导致
泰勒在封盖内被强制拆箱,并因此导致致命错误(拆箱包含
nil的内含物)
“无主”捕获(无主捕获)
“弱者”捕获的替代方法是“无主”。
func sing() -> () -> Void { let taylor = Singer() let singing = { [unowned taylor] in taylor.playSong() return } return singing }
该代码将以类似于可选部署的可选方式的异常结束,后者的可选方式要高一些-无名的
taylor说:“我确定
taylor会在电路关闭时一直存在,因此我不需要将其保存在内存中。” 实际上,
taylor将几乎立即被释放,并且此代码将崩溃。
因此,非常小心地使用
无主物品 。
常见问题
开发人员在闭包中使用价值捕获时面临四个问题:
1.在闭包采用参数的情况下,捕获列表的位置存在困难
这是在研究闭包开始时可能遇到的一个常见问题,但是幸运的是,在这种情况下,Swift可以为我们提供帮助。
当同时使用捕获列表和闭包参数时,捕获列表放在方括号中,然后是闭包参数,然后是in关键字,标记闭包“ body”的开始。
writeToLog { [weak self] user, message in self?.addToLog("\(user) triggered event: \(message)") }
尝试在闭包参数之后放置捕获列表将导致编译错误。
2.出现强链接循环,导致内存泄漏
当实体A具有实体B时,反之亦然,您会遇到一种称为“保留周期”的情况。
作为示例,请考虑以下代码:
class House { var ownerDetails: (() -> Void)? func printDetails() { print("This is a great house.") } deinit { print("I'm being demolished!") } }
我们定义了
House类,该类包含一个属性(关闭),一个方法和一个反初始化器,该反初始化器将在该类的实例被破坏时显示一条消息。
现在创建一个与上一个类相似的
Owner类,不同之处在于其闭包属性包含有关房屋的信息。
class Owner { var houseDetails: (() -> Void)? func printDetails() { print("I own a house.") } deinit { print("I'm dying!") } }
现在,在
do块中创建这些类的实例。 我们不需要catch块,但是使用
do块会在之后立即破坏实例。
print("Creating a house and an owner") do { let house = House() let owner = Owner() } print("Done")
结果,将显示以下消息:“创建房屋和所有者”,“我快死了!”,“我正在被拆毁!”,然后“完成”-一切正常。
现在创建一个强链接循环。
print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = owner.printDetails owner.houseDetails = house.printDetails } print("Done")
现在将显示消息“创建房屋和所有者”,然后显示“完成”。 反初始化器将不会被调用。
这是由于房屋具有指向所有者的财产,而所有者具有指向房屋的财产这一事实而发生的。 因此,它们都不能安全释放。 在实际情况下,这会导致内存泄漏,从而导致性能下降,甚至导致应用程序崩溃。
为了解决这种情况,我们需要创建一个新的闭包并在一种或两种情况下使用“弱”捕获,如下所示:
print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = { [weak owner] in owner?.printDetails() } owner.houseDetails = { [weak house] in house?.printDetails() } } print("Done")
无需声明两个捕获的值,只需在一个地方就可以完成声明-这将使Swift可以在必要时销毁两个类。
在实际项目中,很少出现这种明显的强链接周期的情况,但这更说明了在有能力的开发中使用“弱”捕获的重要性。
3.强链接的无意使用,通常在捕获多个值时
Swift默认情况下使用强大的抓地力,这可能导致意外行为。
考虑以下代码:
func sing() -> () -> Void { let taylor = Singer() let adele = Singer() let singing = { [unowned taylor, adele] in taylor.playSong() adele.playSong() return } return singing }
现在,闭包捕获了两个值,并且以相同的方式使用它们。 但是,只有
taylor被捕获为未拥有
-adele被强烈捕获,因为必须为每个捕获的值使用
unowned关键字。
如果您故意这样做,那么一切都很好,但是如果您希望两个值都被“
无主 ”捕获,则需要满足以下条件:
[unowned taylor, unowned adele]
4.复制闭包并共享捕获的值
开发人员偶然发现的最后一种情况是如何复制错误,因为他们捕获的数据可用于所有错误副本。
考虑一个简单的闭包示例,该示例捕获闭包外部声明的整数变量
numberOfLinesLogged ,以便我们可以增加其值并在每次调用该闭包时将其打印出来:
var numberOfLinesLogged = 0 let logger1 = { numberOfLinesLogged += 1 print("Lines logged: \(numberOfLinesLogged)") } logger1()
这将显示消息“记录的行数:1”。
现在,我们将创建一个闭合的副本,该副本将与第一个闭合一起共享捕获的值。 因此,如果我们调用原始的闭包或其副本,我们将看到变量的价值不断增长。
let logger2 = logger1 logger2() logger1() logger2()
这将打印出消息“记录的行数:1” ...“记录的行数:4”,因为
logger1和
logger2指向相同的捕获的
numberOfLinesLogged变量。
何时使用“强”捕获,“弱”和“无主”
现在,我们了解了一切工作原理,让我们尝试总结一下:
1.如果您确定执行关闭操作时捕获的值永远不会
为零 ,则可以使用
“未拥有的捕获” 。 这是一种罕见的情况,即使使用闭包内部的弱捕获值使用“弱”捕获也会导致其他困难。
2.如果遇到强链接循环的情况(实体A拥有实体B,实体B拥有实体A),则在其中一种情况下,您需要使用
“弱捕获” 。 必须考虑到两个实体中的哪个实体将首先被释放,因此如果视图控制器A表示视图控制器B,则视图控制器B可能包含一个返回到“ A”的“弱”链接。
3.如果排除了出现强链接周期的可能性,则可以使用“强”捕获(
“强捕获” )。 例如,执行动画不会在包含动画的闭包内部阻止自身,因此您可以使用强绑定。
4.如果不确定,请从“弱”绑定开始,然后仅在必要时进行更改。
可选-Swift官方指南:
短路自动链接计数