iOS Swift之defer高级编程

最近在 swift 文档中,发现了defer这个关键字,本人抱着学习态度,查看了一些资料,把我所知道的知识和技能点分享给大家。

苹果官方介绍

用 defer 语句在即将离开当前代码块时执行一系列语句。该语句让你能执行一些必要的清理工作,不管是以何种方式离开当前代码块的——无论是由于抛出错误而离开,或是由于诸如 return、break 的语句。例如,你可以用 defer 语句来确保文件描述符得以关闭,以及手动分配的内存得以释放。
defer 语句将代码的执行延迟到当前的作用域退出之前。该语句由 defer 关键字和要被延迟执行的语句组成。延迟执行的语句不能包含任何控制转移语句,例如 break、return 语句,或是抛出一个错误。延迟执行的操作会按照它们声明的顺序从后往前执行——也就是说,第一条 defer 语句中的代码最后才执行,第二条 defer 语句中的代码倒数第二个执行,以此类推。最后一条语句会第一个执行。

总结一下苹果的介绍

defer语句在代码块(方法、闭包等,可以理解为大括号包装起来的代码)作用域退出之前执行,也就是代码块中其他应该执行的代码都执行完了,才执行defer中的代码
一个代码块允许多个defer,多个defer执行的顺序 从后到前。

defer作用

1.简单来说,用一句话概括,就是 defer block 里的代码会在函数 return 之前执行,无论函数是从哪个分支 return 的,还是有 throw,还是自然而然走到最后一行。
这个关键字就跟 Java 里的 try-catch-finally 的finally一样,不管 try catch 走哪个分支,它都会在函数 return 之前执行。而且它比 Java 的finally还更强大的一点是,它可以独立于 try catch 存在,所以它也可以成为整理函数流程的一个小帮手。在函数 return 之前无论如何都要做的处理,可以放进这个 block 里,让代码看起来更干净。

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

下面是 swift 文档的例子:

var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]
 
func fridgeContains(_ food: String) -> Bool {
    fridgeIsOpen = true
    defer {
        fridgeIsOpen = false
    }
    
    let result = fridgeContent.contains(food)
    return result
}
fridgeContains("banana")
print(fridgeIsOpen)

这个例子里执行的顺序是,先fridgeIsOpen = true,然后是函数体正常的流程,最后在 return 之前执行 fridgeIsOpen = false

几个简单场景

try catch 结构

最典型场景,我想也是defer诞生的原因吧:

func foo() {
  defer {
    print("finally")
  }
  do {
    throw NSError()
    print("impossible")
  } catch {
    print("handle error")
  }
}
  • 不管 do block 是否 throw error,有没有 catch 到,还是 throw 出去了,都会保证在整个函数 return 前执行 defer。在这个例子里,就是先 print 出 "handle error" 再 print 出 "finally"。

do block 里也可以写 defer:

do {
  defer {
    print("finally")
  }
  throw NSError()
  print("impossible")
} catch {
  print("handle error")
}
  • 那么它执行的顺序就会是在 catch block 之前,也就是先 print 出 "finally" 再 print 出 "handle error"。

典型用法

  • Swift 里的 defer 大家应该都很熟悉了,defer 所声明的 block 会在当前代码执行退出后被调用。正因为它提供了一种延时调用的方式,所以一般会被用来做资源释放或者销毁,这在某个函数有多个返回出口的时候特别有用。比如下面的通过 FileHandle 打开文件进行操作的方法:
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 来统一处理。这不仅可以让我们就近在资源申请的地方就声明释放,也减少了未来添加代码时忘记释放资源的可能性:

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)
}

清理工作、回收资源

跟 swift 文档举的例子类似,defer一个很适合的使用场景就是用来做清理工作。

文件操作就是一个很好的例子:

关闭文件

func foo() {
  let fileDescriptor = open(url.path, O_EVTONLY)
  defer {
    close(fileDescriptor)
  }
  // use fileDescriptor...
}

这样就不怕哪个分支忘了写,或者中间 throw 个 error,导致 fileDescriptor 没法正常关闭。

defer 的作用域

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

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 中获取过这个锁了,比如:

func doSomethingThenRemove() {
    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 改成了这样:

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.
所以,上面的代码其实相当于:

func remove(_ id: ID, acquireLock: Bool) {
if acquireLock {
lock.lock()
lock.unlock()
}
tasks[id] = nil
}

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

defer 和闭包

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

func foo() {
    var number = 1
    defer { print("Statement 2: \(number)") }
    number = 100
    print("Statement 1: \(number)")
}
将会输出:
Statement 1: 100
Statement 2: 100

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

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 开始的自增:

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 的目的就是进行资源清理和避免重复的返回前需要执行的代码,而不是用来以取巧地实现某些功能。这样做只会让代码可读性降低。

一些测试及误区纠正。

测试案例1

func testDefer() {
    defer {
        print("方法中defer内容")
    }
    if true {
        defer {
            print("if 中defer内容")
        }
        print("if中最后的代码")
    }
    print("方法中的代码")
    if true {
        return
    }
    print("方法结束前最后一句代码")
}
testDefer()


以上代码打印结果:

if中最后的代码
if 中defer内容
方法中的代码
方法中defer内容
打印结果中,第一个if中的代码及里面的defer最先执行,方法中的defer最后执行,由此可以看出,代码块中其他能够执行的代码先执行,最后执行defer的内容;defer的作用范围不能简单的看成方法,而是代码块(可能有些同学会有这样的误区)

测试案例2

func testDefer() {
    print("开始")
    defer {
        print("defer 1 中的内容")
    }
    defer {
        print("defer 2 中的内容")
    }
    if true {
        return
    }
    defer {
        print("defer 3 中的内容")
    }
    print("方法结束前最后一句代码")
}
testDefer()

打印结果

开始
defer 2 中的内容
defer 1 中的内容
我们可以看到最后一个defer没有执行,所以defer定义的位置很重要,如果没有执行defer定义的代码,在代码块结束前不会执行defer中的内容
多个defer的执行顺序从后到前。

实际应用场景

场景1:一些资源用完后需释放,这里给的是官方的一个案例

func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // 处理文件。
        }
        // close(file) 会在这里被调用,即作用域的最后。
    }
}

开始用到资源的时候就使用defer去释放,避免忘记释放资源

场景2:加锁解锁,借鉴了kingfisher

let lock = NSLock()
func testDefer() {
    lock.lock()
    defer {
        lock.unlock()
    }
    
    doSomething()
}
testDefer()

在加锁后立刻用defer解锁,避免忘记解锁

场景3:处理一些代码块作用域结束前的重复操作,比如请求网络数据的时候

通常的一种写法

func loadCityList(_ finish: ((Error?, [String]?) -> ())?) {
    DispatchQueue.global().async { // 模拟网络请求
        let data: AnyObject? // 模拟服务器返回的数据
        guard let dict = data as? [String: AnyObject] else {
            DispatchQueue.main.async {
                finish?(error, nil)
            }
            return
        }
        guard let code = dict["code"] as? Int, code == 200 else {
            DispatchQueue.main.async {
                finish?(error, nil)
            }
            return
        }
        guard let citys = dict["data"] as? [String]? else {
            DispatchQueue.main.async {
                finish?(error, nil)
            }
            return
        }
        DispatchQueue.main.async {
            finish?(nil, citys)
        }
    }
}

当每次有错误处理时和结果正确时都需要去做回调,而且回调可能有一堆代码,看起来代码会比较冗余,而且在一些错误处理时很容易造成忘记回调

defer怎么去写呢

func loadCityList(_ finish: ((Error?, [String]?) -> ())?) {
    DispatchQueue.global().async { // 模拟网络请求
        var error: Error? = nil
        var citys: [String]? = nil
        defer {
            DispatchQueue.main.async {
                finish?(error, citys)
            }
        }
        
        let data: AnyObject? // 模拟服务器返回的数据
        guard let dict = data as? [String: AnyObject] else {
            error = ...
            return
        }
        guard let code = dict["code"] as? Int, code == 200 else {
            error = ...
            return
        }
        guard let tempCitys = dict["data"] as? [String]? else {
            error = ...
            return
        }
        citys = tempCitys
    }
}

使用defer既解决了代码冗余,又解决了可能忘记回调的问题,还有当我们看到defer时,我们很清楚知道,无论网络请求结果如果,都会回调。

dealloc 手动分配的空间

func foo() {
  let valuePointer = UnsafeMutablePointer.allocate(capacity: 1)
  defer {
    valuePointer.deallocate(capacity: 1)
  }
  // use pointer...
}

加/解锁:下面是 swift 里类似 Objective-C 的 synchronized block 的一种写法,可以使用任何一个 NSObject 作 lock

func foo() {
  objc_sync_enter(lock)
  defer { 
    objc_sync_exit(lock)
  }
  // do something...
}

像这种成对调用的方法,可以用 defer 把它们放在一起,一目了然。

调completion block

这是一个让我感觉“如果当时知道 defer ”就好了的场景,就是有时候一个函数分支比较多,可能某个小分支 return 之前就忘了调 completion block,结果藏下一个不易发现的 bug。用 defer 就可以不用担心这个问题了:

func foo(completion: () -> Void) {
  defer {
    self.isLoading = false
    completion()
  }
  guard error == nil else { return } 
  // handle success
}

有时候 completion 要根据情况传不同的参数,这时 defer 就不好使了。不过如果 completion block 被存下来了,我们还是可以用它来确保执行后能释放:

func foo() {
  defer {
    self.completion = nil
  }
  if (succeed) {
    self.completion(.success(result))
  } else {
    self.completion(.error(error))
  }
}

调 super 方法

有时候 override 一个方法,主要目的是在 super 方法之前做一些准备工作,比如 UICollectionViewLayout 的 prepare(forCollectionViewUpdates:),那么我们就可以把调用 super 的部分放在 defer 里:

func override foo() {
  defer {
    super.foo()
  }
  // some preparation before super.foo()...
}

一些细节

任意 scope 都可以有 defer

虽然大部分的使用场景是在函数里,不过理论上任何一个 { } 之间都是可以写 defer 的。比如一个普通的循环:

var sumOfOdd = 0
for i in 0...10 {
  defer {
    print("Look! It's \(i)")
  }
  if i % 2 == 0 {
    continue
  }
  sumOfOdd += i
}
continue 或者 break 都不会妨碍 defer 的执行。甚至一个平白无故的 closure 里也可以写 defer:

{
  defer { print("bye!") }
  print("hello!")
}

就是这样没什么意义就是了……

必须执行到 defer 才会触发

假设有这样一个问题:一个 scope 里的 defer 能保证一定会执行吗?
答案是否……比如下面这个例子:

func foo() throws {
  do {
    throw NSError()
    print("impossible")
  }
  defer {
    print("finally")
  }
}
try?foo()

不会执行 defer,不会 print 任何东西。这个故事告诉我们,至少要执行到 defer 这一行,它才保证后面会触发。同样道理,提前 return 也是一样不行的:

func foo() {
  guard false else { return }
  defer {
    print("finally")
  }
}

多个 defer

一个 scope 可以有多个 defer,顺序是像栈一样倒着执行的:每遇到一个 defer 就像压进一个栈里,到 scope 结束的时候,后进栈的先执行。如下面的代码,会按 1、2、3、4、5、6 的顺序 print 出来。

func foo() {
  print("1")
  defer {
    print("6")
  }
  print("2")
  defer {
    print("5")
  }
  print("3")
  defer {
    print("4")
  }
}

以上为这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家支持。

青山不改,绿水长流,后会有期,感谢每一位佳人的支持!

你可能感兴趣的:(iOS Swift之defer高级编程)