服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Swift - Swift中defer的正确使用方法

Swift中defer的正确使用方法

2021-01-13 16:41OneV''''s Den Swift

准备把 swift 文档再扫一遍,发现了defer这个关键字,所以下面这篇文章主要给大家介绍了关于Swift中defer的正确使用方法,文中通过示例代码介绍的非常详细,需要的朋友可以参考下

defer 是干什么用的

很简单,用一句话概括,就是 defer block 里的代码会在函数 return 之前执行,无论函数是从哪个分支 return 的,还是有 throw,还是自然而然走到最后一行。

这个关键字就跟 Java 里的 try-catch-finally 的finally一样,不管 try catch 走哪个分支,它都会在函数 return 之前执行。而且它比 Java 的finally还更强大的一点是,它可以独立于 try catch 存在,所以它也可以成为整理函数流程的一个小帮手。在函数 return 之前无论如何都要做的处理,可以放进这个 block 里,让代码看起来更干净一些~

其实这篇文章的缘起是由于在对 Kingfisher 做重构的时候,因为自己对 defer 的理解不够准确,导致了一个 bug。所以想藉由这篇文章探索一下 defer 这个关键字的一些 edge case。

典型用法

Swift 里的 defer 大家应该都很熟悉了,defer 所声明的 block 会在当前代码执行退出后被调用。正因为它提供了一种延时调用的方式,所以一般会被用来做资源释放或者销毁,这在某个函数有多个返回出口的时候特别有用。比如下面的通过 FileHandle 打开文件进行操作的方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func operateOnFile(descriptor: Int32) {
let fileHandle = FileHandle(fileDescriptor: descriptor)
 
let data = fileHandle.readDataToEndOfFile()
 
if /* onlyRead */ {
fileHandle.closeFile()
return
}
 
let shouldWrite = /* 是否需要写文件 */
guard shouldWrite else {
fileHandle.closeFile()
return
}
 
fileHandle.seekToEndOfFile()
fileHandle.write(someData)
fileHandle.closeFile()
}

我们在不同的地方都需要调用 fileHandle.closeFile() 来关闭文件,这里更好的做法是用 defer 来统一处理。这不仅可以让我们就近在资源申请的地方就声明释放,也减少了未来添加代码时忘记释放资源的可能性:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
func operateOnFile(descriptor: Int32) {
let fileHandle = FileHandle(fileDescriptor: descriptor)
defer { fileHandle.closeFile() }
let data = fileHandle.readDataToEndOfFile()
 
if /* onlyRead */ { return }
 
let shouldWrite = /* 是否需要写文件 */
guard shouldWrite else { return }
 
fileHandle.seekToEndOfFile()
fileHandle.write(someData)
}

defer 的作用域

在做 Kingfisher 重构时,对线程安全的保证我选择使用了 NSLock 来完成。简单说,会有一些类似这样的方法:

?
1
2
3
4
5
6
7
8
let lock = NSLock()
let tasks: [ID: Task] = [:]
 
func remove(_ id: ID) {
lock.lock()
defer { lock.unlock() }
tasks[id] = nil
}

对于 tasks 的操作可能发生在不同线程中,用 lock() 来获取锁,并保证当前线程独占,然后在操作完成后使用 unlock() 释放资源。这是很典型的 defer 的使用方式。

但是后来出现了一种情况,即调用 remove 方法之前,我们在同一线程的 caller 中获取过这个锁了,比如:

?
1
2
3
4
5
6
7
8
9
10
func doSomethingThenRemove() {
lock.lock()
defer { lock.unlock() }
 
// 操作 `tasks`
// ...
 
// 最后,移除 `task`
remove(123)
}

这样做显然在 remove 中造成了死锁 (deadlock):remove 里的 lock() 在等待 doSomethingThenRemove 中做 unlock() 操作,而这个 unlock 被 remove 阻塞了,永远不可能达到。

解决的方法大概有三种:

  1. 换用 NSRecursiveLock:NSRecursiveLock 可以在同一个线程获取多次,而不造成死锁的问题。
  2. 在调用 remove 之前先 unlock。
  3. 为 remove 传入按照条件,避免在其中加锁。

1 和 2 都会造成额外的性能损失,虽然在一般情况下这样的加锁性能微乎其微,但是使用方案 3 似乎也并不很麻烦。于是我很开心地把 remove 改成了这样:

?
1
2
3
4
5
6
7
func remove(_ id: ID, acquireLock: Bool) {
if acquireLock {
lock.lock()
defer { lock.unlock() }
}
tasks[id] = nil
}

很好,现在调用 remove(123, acquireLock: false) 不再会死锁了。但是很快我发现,在 acquireLock 为 true 的时候锁也失效了。再仔细阅读 Swift Programming Language 关于 defer 的描述:

A defer statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.

所以,上面的代码其实相当于:

?
1
2
3
4
5
6
7
func remove(_ id: ID, acquireLock: Bool) {
if acquireLock {
lock.lock()
lock.unlock()
}
tasks[id] = nil
}

GG 斯密达…

以前很单纯地认为 defer 是在函数退出的时候调用,并没有注意其实是当前 scope 退出的时候调用这个事实,造成了这个错误。在 if,guard,for,try 这些语句中使用 defer 时,应该要特别注意这一点。

defer 和闭包

另一个比较有意思的事实是,虽然 defer 后面跟了一个闭包,但是它更多地像是一个语法糖,和我们所熟知的闭包特性不一样,并不会持有里面的值。比如:

?
1
2
3
4
5
6
func foo() {
var number = 1
defer { print("Statement 2: \(number)") }
number = 100
print("Statement 1: \(number)")
}

将会输出:

Statement 1: 100
Statement 2: 100

在 defer 中如果要依赖某个变量值时,需要自行进行复制:

?
1
2
3
4
5
6
7
8
9
10
func foo() {
var number = 1
var closureNumber = number
defer { print("Statement 2: \(closureNumber)") }
number = 100
print("Statement 1: \(number)")
}
 
// Statement 1: 100
// Statement 2: 1

defer 的执行时机

defer 的执行时机紧接在离开作用域之后,但是是在其他语句之前。这个特性为 defer 带来了一些很“微妙”的使用方式。比如从 0 开始的自增:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo {
var num = 0
func foo() -> Int {
defer { num += 1 }
return num
}
 
// 没有 `defer` 的话我们可能要这么写
// func foo() -> Int {
// num += 1
// return num - 1
// }
}
 
let f = Foo()
f.foo() // 0
f.foo() // 1
f.num // 2

输出结果 foo() 返回了 +1 之前的 num,而 f.num 则是 defer 中经过 +1 之后的结果。不使用 defer 的话,我们其实很难达到这种“在返回后进行操作”的效果。

虽然很特殊,但是强烈不建议在 defer 中执行这类 side effect。

This means that a defer statement can be used, for example, to perform manual resource management such as closing file descriptors, and to perform actions that need to happen even if an error is thrown.

从语言设计上来说,defer 的目的就是进行资源清理和避免重复的返回前需要执行的代码,而不是用来以取巧地实现某些功能。这样做只会让代码可读性降低。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:https://onevcat.com/2018/11/defer/

延伸 · 阅读

精彩推荐
  • SwiftSwift算法之栈和队列的实现方法示例

    Swift算法之栈和队列的实现方法示例

    Swift语言中没有内设的栈和队列,很多扩展库中使用Generic Type来实现栈或是队列。下面这篇文章就来给大家详细介绍了Swift算法之栈和队列的实现方法,需要...

    李峰峰10002021-01-05
  • Swiftswift相册相机的权限处理示例详解

    swift相册相机的权限处理示例详解

    在iOS7以后要打开手机摄像头或者相册的话都需要权限,在iOS9中更是更新了相册相关api的调用,那么下面这篇文章主要给大家介绍了关于swift相册相机权限处...

    hello老文12682021-01-08
  • SwiftSwift中排序算法的简单取舍详解

    Swift中排序算法的简单取舍详解

    对于排序算法, 通常简单的, 为大家所熟知的有, 选择排序, 冒泡排序, 快速排序, 当然还有哈希, 桶排序之类的, 本文仅比较最为常见的选择, 冒泡和快排,文...

    Castie111012021-01-10
  • Swift浅谈在Swift中关于函数指针的实现

    浅谈在Swift中关于函数指针的实现

    这篇文章主要介绍了浅谈在Swift中关于函数指针的实现,是作者根据C语言的指针特性在Swifft中做出的一个实验,需要的朋友可以参考下...

    Swift教程网4372020-12-21
  • SwiftSwift网络请求库Alamofire使用详解

    Swift网络请求库Alamofire使用详解

    这篇文章主要为大家详细介绍了Swift网络请求库Alamofire的使用方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    lv灬陈强56682021-01-06
  • Swift详解Swift 之clipped是什么如何用

    详解Swift 之clipped是什么如何用

    这篇文章主要介绍了详解Swift 之clipped是什么如何用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下...

    iCloudEnd8532021-05-28
  • Swift分析Swift性能高效的原因

    分析Swift性能高效的原因

    绝大多数公司选择Swift语言开发iOS应用,主要原因是因为Swift相比Objc有更快的运行效率,更加安全的类型检测,更多现代语言的特性提升开发效率;这一系...

    louis_wang9092021-01-16
  • SwiftSwift 基本数据类型详解总结

    Swift 基本数据类型详解总结

    在我们使用任何程序语言编程时,需要使用各种数据类型来存储不同的信息。变量的数据类型决定了如何将代表这些值的位存储到计算机的内存中。在声明...

    Lucky_William4672021-12-26