Swift 作为现代、高效、安全的编程言语,其背面有很多高级特性为之支撑。
『 Swift 最佳实践 』系列对常用的言语特性逐一进行介绍,助力写出更简练、更优雅的 Swift 代码,快速实现从 OC 到 Swift 的改变。
该系列内容首要包含:
- Optional
- Enum
- Closure
- Protocol
- Generic
- Property Wrapper
- Structured Concurrent
- Result builder
- Error Handle
- Advanced Collections (Asyncsequeue/OptionSet/Lazy)
- Expressible by Literal
- Pattern Matching
- Metatypes(.self/.Type/.Protocol)
ps. 本系列不是入门级语法教程,需要有一定的 Swift 根底
本文是系列文章的第三篇,介绍 Closure,内容首要包含如何利用 Inferring Type 简化闭包的运用、escaping-closure 与 nonescaping-closure 的差异、Capture List 注意事项、Trailing Closures 以及 Auto Closures 等。
Overview
Swift Closure 与 Objective-C Block 有很多相似之处,都属于匿名函数 / lambdas-expressions 的范畴。
相比之下,Swift Closure 更安全、更简练。
首要,扼要回忆一下闭包的基本语法,Closure 完好界说如下,几个关键组成:
- 参数列表
- 回来值类型
- 关键字
in
- closure body statements
{ (parameters) -> type in
statements
}
声明 Closure 变量:
let closure: (parameters) -> type
看个简略的例子,如下,为数组排序办法 sorted
传入了用于排序操作的 closure,其类型为:(String, String) -> Bool
,即有 2 个 String
类型的参数,回来值为 Bool
:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
let reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
因为 closure body 只要一行代码,故能够直接将其放在 in
后边:
names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })
Inferring Type
得益于 Swift 强大的类型推演能力,上述排序闭包能够简化为:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
即不用显式写明参数、回来值的类型,编译器依据上下文完全能够推演出来
从 Swift 5.1 起,关于只要一个表达式 (Single-expression) 的办法 / 闭包,会隐式回来该表达式的值 swift-evolution/0255-omit-return GitHub
简略讲,便是关于 Single-expression 的办法 / 闭包能够省掉 return
关键字:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
Swift 为 Closure 参数供给了一种简写方式,即分别用 $0
、$1
来表示参数列表中的参数,此刻能够忽略闭包参数列表,如下:
reversedNames = names.sorted(by: { $0 > $1 } )
// 如后文所述还有更简练的版本
// reversedNames = names.sorted(by: >)
关于只要一个参数的闭包能够运用参数的简写方式
$0
关于有 2 个及以上参数的状况慎用简写方式,可能会影响代码可读性
Escaping Closures
escaping-closure、nonescaping-closure 是 Swift Closure 相较 OC Block 出现的一个新概念。
escaping、nonescaping 描绘的是 Closure 作为办法参数时的分类:
-
当作为参数的 Closure,其生命周期不会逃逸出所在办法时,称为 nonescaping-closure,(意味着该闭包在办法调用链上会被履行),如:
func foo(_ closure: () -> Void) { // closure 没有逃逸出 foo closure() }
如下,虽在办法
bar
中没有直接履行closure
,但在其调用链上的foo
会履行closure
,closure
并没有逃逸出bar
:func bar(_ closure: () -> Void) { foo(closure) }
-
当 Closure 的生命周期逃逸出所在办法时,称为 escaping-closure。
如下,
foo
将参数escapingClosure
存储在属性closure
中,使其逃逸出foo
,即foo
回来后escapingClosure
还存在:public class EscapingClosureDemo { var closure: (() -> Void)? func foo(_ escapingClosure: @escaping () -> Void) { closure = escapingClosure } }
此刻,参数
escapingClosure
的界说须加上关键字@escaping
,显式表明界说的是 escaping-closure,不然编译报错:OC 中相当于所有 block 默许都是 escaping
在 Swift 3 以前, 闭包类型的参数默许是 escaping,并供给了
@noescape
关键字用于声明 nonescaping Closure但从 Swift 3 开端,闭包参数默许是 nonescaping,关于 escaping closure 须显式声明
@escaping
,并抛弃了@noescape
swift-evolution/0103-make-noescape-default GitHub
Why❓
为什么要区分 escaping、nonescaping,并在 Swift 3 中将默许值从 escaping 改成 nonescaping?
首要原因有两个:
- 编译器优化 ,关于 escaping closure 需要更杂乱的内存办理,而 nonescaping closure 编译器能够做优化
- 显式提醒开发人员 ⚡️:「你正在风险的边际试探——正在界说 / 调用的是 escaping closure!」
escaping-closure 为何就风险了❓
原因在于 escaping-closure 可能会发生循环引证 (Strong Reference Cycles),而 nonescaping closure 一定是不会有循环引证的。
因此,在 escaping-closure 中不允许隐式捕获 self
,避免在「不经意间 」引起循环引证:
Capturing Values
我们从一个简略的问题开端:Can You Answer This Simple Swift Question Correctly?
在 1334 位答复者中只要 44% 答复正确
正确答案:
- 1 — Objc
- 2 — Swift
1 和 2 的差异在于:1 用了捕获列表 (Capture List),而 2 没有。
Capture List:
-
用
[]
声明的表达式列表,表达式间用,
分隔,放在参数列表前 (如有):func bar() { var age = 10 var name = "Jim" let closure = { [age, name] in // 等价于 [age = age, name = name] // 此处的 age、name 与闭包外的已没任何关系,仅名字相同而以 print("(name) is (age) years old!") } age = 11 name = "Tom" closure() // Jim is 10 years old! }
-
capture list 在闭包界说时完结初始化赋值 (
[age = age, name = name]
)所以上面输出的是
Jim is 10 years old!
,而不是Tom is 11 years old!
假如不用 capture list,而是直接引证,则直到 closure 履行时才去取值:
but,假如捕获的是引证类型 (reference types),那状况就不一样了:
String 在 Swift 中是值类型,而非引证类型
class Foo { var age: Int var name: String init(age: Int, name: String) { self.age = age self.name = name } } func bar() { var foo = Foo(age: 10, name: "Jim") let closure = { [foo] in // 捕获引证类型 (foo) print("(foo.name) is (foo.age) years old!") } foo.age = 11 foo.name = "Tom" closure() // Tom is 11 years old! }
虽然,捕获引证类型时,其输出有所不同,但「capture list 在闭包界说时完结初始化赋值」的特性并没有变,只不过此刻赋值的是「指针」
如下,若给
foo
赋一个新值,则与 closure 内捕获的就没任何关系了:func bar() { var foo = Foo(age: 10, name: "Jim") let closure = { [foo] in print("(foo.name) is (foo.age) years old!") } foo = Foo(age: 11, name: "Tom") // 此刻,foo 指向新实例 closure() // Jim is 10 years old! }
-
通过 capture list 还能够指定捕获类型:
strong
(默许)、weak
以及unowned
,用于避免循环引证:{ print(self.name) } // implicit strong capture { [self] in print(self.name) } // explicit strong capture { [weak self] in print(self?.name) } // weak capture { [unowned self] in print(self.name) } // unowned capture // 还能够在 capture list 用表达式赋值 { [name = self.name] in print(name)} // strong capture name
总之,capture list 在闭包界说时完结初始化赋值 (而非履行时):
- 关于值类型 (value types),capture value 初始化实质上便是 copy,从此以后闭包内外的值就各奔前程,没任何关系了 (仅仅名字相同罢了)
- 关于引证类型 (reference types),capture value 初始化 copy 的实质上是个指针,闭包内外引证的仍是同一个实例
Trailing Closures
假如办法的最后一个参数是 closure,那么在调用时能够简化:
func foo() {
// 正常调用
bar(doSomething: {
print("normal call!")
})
// trailing closure call
// 省掉()、label
bar {
print("trailing closure call!")
}
}
func bar(doSomething: () -> Void) {
doSomething()
}
关于有多个 closure 参数的状况,建议只对最后一个参数用 trailing closure call,避免影响可读性:
func foo() {
// ❎ 不建议
bar {
print("1")
} doSomething1: {
print("2")
}
// ✅
bar(doSomething: {
print("1")
}) {
print("2")
}
}
func bar(doSomething: () -> Void, doSomething1: () -> Void) {
doSomething()
doSomething1()
}
Auto closures
如下,给 closure 加上 @autoclosure
后,在调用时能够直接用表达式,传入的表达式会主动封装成 closure,而无需显式的写成闭包的方式:
func foo() {
bar(doSomething: print("a")) // ❌ bar(doSomething: { print("a") })
baz(value: 1) // ❌ baz(value: { 1 })
}
func bar(doSomething: @autoclosure () -> Void) {
print("b")
doSomething()
}
func baz(value: @autoclosure () -> Int) {
print(value())
}
// 输出:
// b
// a
// 1
- 对 closure 加
@autoclosure
的条件是其没有参数 - 表达式是 lazy,只要到闭包履行时才履行表达式,而非办法调用时
正是因为 @autoclosure
的 lazy 特性,其常被用于那些期望懒加载的场景,如:Swift 标准库供给的 assert
函数:
public func assert(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String = String(),
file: StaticString = #file, line: UInt = #line
) {
// Only assert in debug mode.
if _isDebugAssertConfiguration() {
if !_fastPath(condition()) {
_assertionFailure("Assertion failed", message(), file: file, line: line, flags: _fatalErrorFlags())
}
}
}
因为 condition
、message
是 auto closure,故能够简化 assert
调用:
assert(someCondition(), "Failed!")
假如它们是常规闭包,则调用时有点麻烦:
assert({ someCondition() }, { "Failed!" })
相似的,如 Alamofire 中对 Error 的扩展:
在项目中经常会对 Dictionary
的取值做些保护并供给默许值,如:
extension Dictionary {
func value<T>(forKey key: Key, defaultValue: @autoclosure () -> T) -> T {
self[key].flatMap { $0 as? T } ?? defaultValue()
}
}
First-class functions first, closures second
这一末节有点挖墙脚的意思:「优先考虑一等函数,其次才是闭包」,条件是闭包参数与函数参数匹配,如:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
// closure
let reversedNames = names.sorted(by: { $0 > $1 } )
// first-class function
let reversedNames = names.sorted(by: >)
let domain: String? = "docs.swift.org"
// if let
var url: URL?
if let domain {
url = URL(string: domain)
}
// closure
let url = domain.flatMap { URL(string: $0) }
// first-class function
let url = domain.flatMap(URL.init) // 等价于 domain.flatMap(URL.init(string:))
let subviews: [UIView] = [...]
// closure
subviews.forEach { addSubview($0) }
// first-class functions
subviews.forEach(addSubview)
let ages = [1, 2, 3]
// closure
ages.forEach { baz(value: $0) }
// first-class functions
ages.forEach(baz(value:))
func baz(value: Int) {
print(value)
}
小结
本文对 Closure 的首要特性以及最佳实践进行了扼要分析介绍。
如,利用 Inferring Type 能够简化闭包的运用,Capture List 关于值类型和引证类型的差异,Trailing Closures 以及 Auto closures 的运用等。
最后,提到某些场景下利用函数一等公民身份相比闭包能够简化代码。
参考资料
Apple-Documentation-closures
Swift clip: First class functions
Swift’s closure capturing mechanics
Using @autoclosure when designing Swift APIs
swift-evolution/0255-omit-return GitHub
swift-evolution/0103-make-noescape-default GitHub
swift/Assert.swift at main apple/swift GitHub