Swift中defer的正确使用方法
defer是干什么用的
很简单,用一句话概括,就是deferblock里的代码会在函数return之前执行,无论函数是从哪个分支return的,还是有throw,还是自然而然走到最后一行。
这个关键字就跟Java里的try-catch-finally的finally一样,不管trycatch走哪个分支,它都会在函数return之前执行。而且它比Java的finally还更强大的一点是,它可以独立于trycatch存在,所以它也可以成为整理函数流程的一个小帮手。在函数return之前无论如何都要做的处理,可以放进这个block里,让代码看起来更干净一些~
其实这篇文章的缘起是由于在对Kingfisher做重构的时候,因为自己对defer的理解不够准确,导致了一个bug。所以想藉由这篇文章探索一下defer这个关键字的一些edgecase。
典型用法
Swift里的defer大家应该都很熟悉了,defer所声明的block会在当前代码执行退出后被调用。正因为它提供了一种延时调用的方式,所以一般会被用来做资源释放或者销毁,这在某个函数有多个返回出口的时候特别有用。比如下面的通过FileHandle打开文件进行操作的方法:
funcoperateOnFile(descriptor:Int32){ letfileHandle=FileHandle(fileDescriptor:descriptor) letdata=fileHandle.readDataToEndOfFile() if/*onlyRead*/{ fileHandle.closeFile() return } letshouldWrite=/*是否需要写文件*/ guardshouldWriteelse{ fileHandle.closeFile() return } fileHandle.seekToEndOfFile() fileHandle.write(someData) fileHandle.closeFile() }
我们在不同的地方都需要调用fileHandle.closeFile()来关闭文件,这里更好的做法是用defer来统一处理。这不仅可以让我们就近在资源申请的地方就声明释放,也减少了未来添加代码时忘记释放资源的可能性:
funcoperateOnFile(descriptor:Int32){ letfileHandle=FileHandle(fileDescriptor:descriptor) defer{fileHandle.closeFile()} letdata=fileHandle.readDataToEndOfFile() if/*onlyRead*/{return} letshouldWrite=/*是否需要写文件*/ guardshouldWriteelse{return} fileHandle.seekToEndOfFile() fileHandle.write(someData) }
defer的作用域
在做Kingfisher重构时,对线程安全的保证我选择使用了NSLock来完成。简单说,会有一些类似这样的方法:
letlock=NSLock() lettasks:[ID:Task]=[:] funcremove(_id:ID){ lock.lock() defer{lock.unlock()} tasks[id]=nil }
对于tasks的操作可能发生在不同线程中,用lock()来获取锁,并保证当前线程独占,然后在操作完成后使用unlock()释放资源。这是很典型的defer的使用方式。
但是后来出现了一种情况,即调用remove方法之前,我们在同一线程的caller中获取过这个锁了,比如:
funcdoSomethingThenRemove(){ lock.lock() defer{lock.unlock()} //操作`tasks` //... //最后,移除`task` remove(123) }
这样做显然在remove中造成了死锁(deadlock):remove里的lock()在等待doSomethingThenRemove中做unlock()操作,而这个unlock被remove阻塞了,永远不可能达到。
解决的方法大概有三种:
- 换用NSRecursiveLock:NSRecursiveLock可以在同一个线程获取多次,而不造成死锁的问题。
- 在调用remove之前先unlock。
- 为remove传入按照条件,避免在其中加锁。
1和2都会造成额外的性能损失,虽然在一般情况下这样的加锁性能微乎其微,但是使用方案3似乎也并不很麻烦。于是我很开心地把remove改成了这样:
funcremove(_id:ID,acquireLock:Bool){ ifacquireLock{ lock.lock() defer{lock.unlock()} } tasks[id]=nil }
很好,现在调用remove(123,acquireLock:false)不再会死锁了。但是很快我发现,在acquireLock为true的时候锁也失效了。再仔细阅读SwiftProgrammingLanguage关于defer的描述:
Adeferstatementisusedforexecutingcodejustbeforetransferringprogramcontroloutsideofthescopethatthedeferstatementappearsin.
所以,上面的代码其实相当于:
funcremove(_id:ID,acquireLock:Bool){ ifacquireLock{ lock.lock() lock.unlock() } tasks[id]=nil }
GG斯密达…
以前很单纯地认为defer是在函数退出的时候调用,并没有注意其实是当前scope退出的时候调用这个事实,造成了这个错误。在if,guard,for,try这些语句中使用defer时,应该要特别注意这一点。
defer和闭包
另一个比较有意思的事实是,虽然defer后面跟了一个闭包,但是它更多地像是一个语法糖,和我们所熟知的闭包特性不一样,并不会持有里面的值。比如:
funcfoo(){ varnumber=1 defer{print("Statement2:\(number)")} number=100 print("Statement1:\(number)") }
将会输出:
Statement1:100
Statement2:100
在defer中如果要依赖某个变量值时,需要自行进行复制:
funcfoo(){ varnumber=1 varclosureNumber=number defer{print("Statement2:\(closureNumber)")} number=100 print("Statement1:\(number)") } //Statement1:100 //Statement2:1
defer的执行时机
defer的执行时机紧接在离开作用域之后,但是是在其他语句之前。这个特性为defer带来了一些很“微妙”的使用方式。比如从0开始的自增:
classFoo{ varnum=0 funcfoo()->Int{ defer{num+=1} returnnum } //没有`defer`的话我们可能要这么写 //funcfoo()->Int{ //num+=1 //returnnum-1 //} } letf=Foo() f.foo()//0 f.foo()//1 f.num//2
输出结果foo()返回了+1之前的num,而f.num则是defer中经过+1之后的结果。不使用defer的话,我们其实很难达到这种“在返回后进行操作”的效果。
虽然很特殊,但是强烈不建议在defer中执行这类sideeffect。
Thismeansthatadeferstatementcanbeused,forexample,toperformmanualresourcemanagementsuchasclosingfiledescriptors,andtoperformactionsthatneedtohappenevenifanerroristhrown.
从语言设计上来说,defer的目的就是进行资源清理和避免重复的返回前需要执行的代码,而不是用来以取巧地实现某些功能。这样做只会让代码可读性降低。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。