一、函数类型
在 Swift 中函数本身也有自己的类型,它由形式参数类型,返回类型组成。在使用函数作为变量时,如果有同名函数不指定类型会报错。
那么函数类型的本质是什么呢,我们打开源码,在 Metadata.h 文件中找到 TargetFunctionTypeMetadata:
注意看,TargetFunctionTypeMetadata 继承自 TargetMetadata,那么它必然有 Kind,而它自身又拥有 Flags 和 ResultType,ResultType 是返回值类型的元数据。接下来我们看到 getParameters 函数,这个函数通过 reinterpret_cast 将 (this + 1) 强制转换成 Parameter * 类型,注意!它返回的是指针类型。所以这个函数返回的是一块连续的内存空间,这一块连续的内存空间存储的是 Parameter 类型的数据。
关于 (this + 1) 可以参考《元类型以及 Mirror 源码和 HandyJson 分析还原枚举、结构体、类的 Metadata》这篇文章的第五点。那么 TargetFunctionTypeMetadata 的结构还原如下:
struct TargetFunctionTypeMetadata {
var Kind: Int
var Flags: Int
var ResultType: Any.Type
var parameters: ParametersBuffer<Any.Type>
}
struct ParametersBuffer<Element>{
var element: Element
mutating func buffer(n: Int) -> UnsafeBufferPointer<Element> {
return withUnsafePointer(to: &self) {
let ptr = $0.withMemoryRebound(to: Element.self, capacity: 1) { start in
return start
}
return UnsafeBufferPointer(start: ptr, count: n)
}
}
mutating func index(of i: Int) -> UnsafeMutablePointer<Element> {
return withUnsafePointer(to: &self) {
return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
}
}
}
TargetFunctionTypeMetadata 的结构还原出来了之后,我们想获取参数的个数,在源码中是这么获取的:
Flags 的类型是 TargetFunctionTypeFlags,TargetFunctionTypeFlags 的结构如下:
它的 getNumParameters 方法如下:
TargetFunctionTypeFlags 存储的是一些掩码信息,通过阅读这个源码,得知 Data & NumParametersMask 得到的就是参数的个数,那么这个 Data 不就是 Flags 么,因为 TargetFunctionTypeFlags 只有 Data 一个成员变量。
此时,将 getNumParameters 函数还原出来的结果如下:
func getNumParameters() -> Int { self.Flags & 0x0000FFFF }
接下来我们试着打印 TargetFunctionTypeMetadata 存储的信息,代码如下:
func add(_ a: Double, _ b: Int) -> Double {
return a + Double(b)
}
let functionType = unsafeBitCast(type(of: add) as Any.Type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
let numParameters = functionType.pointee.getNumParameters()
print("函数参数的个数:\(numParameters)")
for i in 0..<numParameters {
print("第\(i)个参数的类型:\(functionType.pointee.parameters.index(of: i).pointee)")
}
print("函数参数的返回值类型:\(functionType.pointee.ResultType)")
打印结果:
函数参数的个数:2
第0个参数的类型:Double
第1个参数的类型:Int
函数参数的返回值类型:Double
二、闭包表达式
在 Swift 中,可以通过 func 定义一个函数,也可以通过闭包表达式定义一个函数。
1. 闭包表达式的书写
闭包表达式是由花括号、参数列表、返回值类型、in 以及函数体构成的,其书写如下:
{
(参数列表) -> 返回值类型 in
函数体代码
}
通过 {} 的方式开始,然后 in 前面的分别是参数列表和返回值类型,in 的后面是函数体代码。
闭包表达式定义的函数如下:
var add = {
(a: Int, b: Int) -> Int in
return a + b
}
func 定义的函数如下:
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
不管那种方式,在调用的时候都是一样的,结果也是一样的。
print(add(10, 20)) // 30
2. 闭包表达式的简写
我们定义一个函数 - exec,这个函数接收三个参数,第一个参数和第二个参数的类型为 Int 类型,第三个参数的类型为函数类型。这个函数类型传两个 Int 类型的参数,返回 Int 类型的参数。
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
以下是这个函数的几种调用方式:
// 第一种
exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
return v1 + v2
})
// 第二种
exec(v1: 10, v2: 20, fn: { v1, v2 in
return v1 + v2
})
// 第三种
exec(v1: 10, v2: 20, fn: { v1, v2 in
v1 + v2
})
// 第四种
exec(v1: 10, v2: 20, fn: { $0 + $1 })
// 第五种
exec(v1: 10, v2: 20, fn: +)
-
第一种写法没有任何的简写,闭包表达式的参数名、参数类型、返回值类型和函数体全部写上。
-
第二种写法相对于第一种写法省略了参数类型和返回值类型。
-
第三种写法相对于第二种写法省略了 return,在 Swift 中,如果函数中返回值的代码很简单的话可以省略 return(就是在函数体中只有一句 return 的代码)。
-
第四种写法相对于第三种写法省略了参数名和 in,因为函数体比较简单,所以可以直接用 1 分别代表 v1 和 v2。
-
第五种写法相对于第四种写法省略了函数体,因为函数体的实现过于简单,只是两个参数相加,所以可以直接用 + 表示。
3. 尾随闭包
如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式。 例如第 3 点的 exec 函数,调用的时候,可以使用尾随闭包:
exec(v1: 10, v2: 20) { v1, v2 in
return v1 + v2
}
如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数后面加圆括号。
func exec(fn: (Int, Int) -> Int) {
print(fn(10, 20))
}
exec(fn: {$0 + $1})
exec() {$0 + $1}
exec {$0 + $1}
三、闭包
一个函数和它所捕获的变量\常量环境组合起来,称为闭包。一般指定义在函数内部的函数,它捕获的是外层函数的局部变量\常量。
1. 捕获局部变量
如下代码,我们用 typealias 定义一个函数类型 Fn,再定义一个 getFn 的函数,如下:
图中红色框框的,组合起来称之为闭包。num 是一个局部变量,调用 getFn 函数来看一下它的变化:
let fn = getFn()
print(fn(1)) // 1
print(fn(1)) // 2
print(fn(1)) // 3
每次调用 fn,传的值都为 1,但是每次打印都是不一样的,感觉就像是 num 这个局部变量被放到堆空间存储起来了,在每次调用的时候都是 num += 1。我们通过汇编来看一下 getFn 的调用情况。
注意看,在 getFn 的汇编中,调用了 swift_allocObject 方法,这个方法在干什么,在申请并分配堆空间的内存。所以实际上闭包会开辟堆空间的内存,把 num 的值放到堆空间上,当每次调用 fn 的时候,都会去堆空间访问这个值,然后进行 += 的操作。
怎么知道这个 num 值在堆空间呢,在汇编调试中的 swift_allocObject 方法后面打下一个断点,然后读取开辟堆空间的内存,如图:
接下来我们将断点打在 return num 处,然后将断点过掉,格式化输出 0x0000000101019d10,如图:
格式化输出堆空间的内存后发现,num 的值存储在 0x101019d20 这个内存地址,它确实存储在堆空间。
2. 捕获全局变量
当函数捕获一个局部变量/常量时,会开辟堆空间的内存去存储这个局部变量/常量,那如果捕获的是一个全局变量呢,会开辟堆空间吗。代码如下:
我们将断点打在 return plus 和 return num 处,先来来看一下 getFn 在汇编的调用情况。
可以看到,在 getFn 的汇编里并没有产生任何的堆空间开辟,它是直接将 plus 函数的地址返回出去,我们接下来再来看一下 plus 函数的汇编代码:
注意看,在 plus 函数中,它是直接拿到全局变量 num 直接修改的。所以函数不会去捕获全局变量/常量,因此这种行为严格上也不叫做闭包。
四、闭包的本质
在探索闭包的本质的时候,需要借助 IR 的代码进行分析,所以我们先来熟悉一下 IR 的部分语法。
1. IR 的语法
数组:
[<elementnumber> x <elementtype>]
//example
alloca [24 x i8], align 8 24个i8都是0
alloca [4 x i32] === array
结构体:
%swift.refcounted = type { %swift.type*, i64 }
//表示形式
%T = type {<type list>} //这种和C语言的结构体类似
指针类型:
<type> *
//example
i64* //64位的整形
getelementptr 指令:
LLVM中我们获取数组和结构体的成员,通过 getelementptr ,语法规则如下:
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx
这里我们看 LLVM 官网当中的一个例子:
struct munger_struct {
int f1;
int f2;
};
void munge(struct munger_struct *P) {
P[0].f1 = P[1].f1 + P[2].f2;
}
getelementptr inbounds %struct.munger_struct, %struct.munger_struct %1, i64
getelementptr inbounds %struct.munger_struct, %struct.munger_struct %1, i32
int main(int argc, const char * argv[]) {
int array[4] = {1, 2, 3, 4};
int a = array[0];
return 0;
}
其中 int a = array[0] 这句对应的LLVM代码应该是这样的:
a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i32 0, i32 0
总结如下:
-
第一个索引不会改变返回的指针的类型,即ptrval前面对应什么类型,返回的就是什么类型。
-
第一个索引的偏移量是由第一个索引的值和第一个ty指定的基本类型共同确定的。
-
第二个索引是在数组或者结构体内进行索引,内部偏移多少元素大小。
-
每增加一个索引,就会使得该索引使用的基本类型和返回的指针类型去掉一层。
例如获取 [4 x i32] 数组地址中第一个所有去除的类型是 [4 x i32] 第二个索引获取的类型是 i32。
2. IR 分析闭包
代码如下:
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
}
let fn = getFn()
2.1. main 函数分析
我们将当前的 main.swift 文件编译成 main.ll 文件,编译方式及的命令在这篇文章:《方法》,生成 main.ll 文件之后,我们打开,找到 main 函数,如下:
define i32 @main(i32 %0, i8** %1) #0 {
entry:
%2 = bitcast i8** %1 to i8*
// 这里调用 main 函数中的 getFn 函数,它的返回值在 IR 中变成了 { i8*, %swift.refcounted* }。
%3 = call swiftcc { i8*, %swift.refcounted* } @"main.getFn() -> (Swift.Int) -> Swift.Int"()
%4 = extractvalue { i8*, %swift.refcounted* } %3, 0
%5 = extractvalue { i8*, %swift.refcounted* } %3, 1
store i8* %4, i8** getelementptr inbounds (%swift.function, %swift.function* @"main.fn : (Swift.Int) -> Swift.Int", i32 0, i32 0), align 8
store %swift.refcounted* %5, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"main.fn : (Swift.Int) -> Swift.Int", i32 0, i32 1), align 8
ret i32 0
}
注意看!在 %3 这一行调用了 getFn 函数,并且它的返回值是一个 { i8*, %swift.refcounted* },全局搜索这个返回值,搜索的结果如下:
%swift.function = type { i8*, %swift.refcounted* }
%swift.refcounted = type { %swift.type*, i64 }
%swift.type = type { i64 }
%swift.full_boxmetadata = type { void (%swift.refcounted*)*, i8**, %swift.type, i32, i8* }
根据 IR 的语法进行分析:
-
{ i8*, %swift.refcounted* } 是一个结构体,这个结构体包含两个成员变量,分别为 i8* 类型的成员和 %swift.refcounted* 类型的成员。
-
%swift.refcounted* 是一个结构体指针,它的结构为 { %swift.type*, i64 },这个结构体包含两个成员变量,分别为 %swift.type* 类型的成员和 i64 类型的成员。
-
%swift.type* 是一个结构体指针,它的结构为 { i64 },它只包含 i64 类型的成员变量。
-
%swift.full_boxmetadata 应该是一个独属于闭包的 metadata,在下面的开辟堆空间的时候,swift_allocObject 传的就是这个玩意儿。
2.2. getFn 函数分析
getFn 的函数实现如下:
define hidden swiftcc { i8*, %swift.refcounted* } @"main.getFn() -> (Swift.Int) -> Swift.Int"() #0 {
entry:
%num.debug = alloca %TSi*, align 8
%0 = bitcast %TSi** %num.debug to i8*
call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)
// 调用 swift_allocObject,创建一个实例,根据第一篇的《结构体与类》,中得知,它返回的是一个 HeapObject * 的结构体指针。所以,%swift.refcounted* 应该是一个 HeapObject *。
// swift_allocObject 的第一个参数要求传的是 metadata,那么 i64 24 和 i64 7 对应的应该是分配内存的大小和内存对齐。
%1 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1
// 将 swift_allocObject 返回的 HeapObject * 强制转换成 { %swift.refcounted, [8 x i8] }
%2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
// 将局部变量 num 的值存储到 { %swift.refcounted, [8 x i8] } 这个结构体。
%3 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %2, i32 0, i32 1
%4 = bitcast [8 x i8]* %3 to %TSi*
store %TSi* %4, %TSi** %num.debug, align 8
%._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
store i64 0, i64* %._value, align 8
%5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %1) #1
call void @swift_release(%swift.refcounted* %1) #1
// insertvalue 是插入、存储值。
// bitcast 是强制转换的意思,将 plus 函数的地址强制转换成 i8*,然后将这个值插入到 { i8*, %swift.refcounted* } 这个结构体的 i8*。
// 然后将上面的 { %swift.refcounted, [8 x i8] } 存储到 { i8*, %swift.refcounted* } 这个结构体的 %swift.refcounted* 这个位置。
%6 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (i64, %swift.refcounted*)* @"partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in main.getFn() -> (Swift.Int) -> Swift.Int" to i8*), %swift.refcounted* undef }, %swift.refcounted* %1, 1
// 将 { i8*, %swift.refcounted* } 结构返回。
ret { i8*, %swift.refcounted* } %6
}
注意看 %1 这一行,调用 swift_allocObject,创建一个实例,根据第一篇的《结构体与类》,中得知,它返回的是一个 HeapObject * 的结构体指针。所以,%swift.refcounted* 应该是一个 HeapObject *。swift_allocObject 的第一个参数要求传的是 metadata,那么 i64 24 和 i64 7 对应的应该是分配内存的大小和内存对齐。
中间的部分做了一些强制转换和赋值 swift_allocObject 返回的 HeapObject * 最终变成了 { %swift.refcounted, [8 x i8] } 这个结构。
看到 %6 这一行。这一行在干什么,在把 plus 函数的地址和 { %swift.refcounted, [8 x i8] } 存储到 { i8*, %swift.refcounted* } 这个结构体,最后将 { i8*, %swift.refcounted* } 返回。
3. 闭包的结构还原
通过以上分析得知:
-
闭包本质上就是一个 { i8*, %swift.refcounted* } 这样的结构,i8* 存储的是函数的地址,%swift.refcounted* 存储的是一个 box *({ %swift.refcounted, [8 x i8] })。
-
而 box * 里有 HeapObject * 和一个 64 位的 value。
-
HeapObject * 我们就比较熟悉,分别存储 metadata 和 refcount。
闭包最终的结构就可以还原出来了,还原出来的结构如下:
struct ClosureData<Box> {
/// 函数地址
var ptr: UnsafeRawPointer
/// 存储捕获堆空间地址的值
var object: UnsafePointer<Box>
}
struct Box<T>{
var heapObject: HeapObject
// 捕获变量/常量的值
var value: T
}
struct HeapObject {
var matedata: UnsafeRawPointer
var refcount: Int
}
用代码验证一下:
//用结构体包裹一下闭包方便作指针的转换
struct ClosureStruct {
var closure :(Int) -> Int
}
var fn = ClosureStruct(closure: getFn())
fn.closure(10)
//fn 初始化一个ClosureStruct类型指针
let ptr = UnsafeMutablePointer<ClosureStruct>.allocate(capacity: 1)
ptr.initialize(to: fn)
//内存重新绑定为 ClosureData<Box<Int>>
let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1){
$0.pointee
}
print("闭包的调用地址:",ctx.ptr)
print("堆空间地址:",ctx.object)
print("堆空间存储的值", ctx.object.pointee.value)
ptr.deinitialize(count: 1)
ptr.deallocate()
为了验证打印出的函数地址是否就是 plus 函数的地址,我们可以在 return plus 处打一个断点,然后通过汇编来读取 plus 函数的地址,如图:
可以看到 plus 函数的地址为 0x0000000100002c00,我们放开断点,继续执行,测试代码打印出的函数地址和堆空间的地址,如图:
plus 函数的地址验证成功,捕获的变量的值也存储在堆空间,并且,这个值在 HeapObject 之后,所以我们还原出来的结构是正确的。
4. 捕获引用类型
捕获一个值类型,会在堆空间开辟内存,那么捕获引用类型呢,我们通过汇编来分析,代码如下:
typealias Fn = (Int) -> Int
class SHPerson {
var age = 0
}
func getFn() -> Fn {
let person = SHPerson()
func plus(_ i: Int) -> Int {
person.age += i
return person.age
}
return plus
}
let fn = getFn()
我们在 return plus 和 person.age += i 处打一个断点,先来看一下 getFn 函数的汇编实现情况:
可以看到,唯一开辟的堆空间的无非就是 SHPerson 的初始化,那么它捕获的是什么呢,我们通过读取 rax 的值:0x000000010126c330,这个值就是 person 的内存地址。接下来放开断点,用我们的测试代码将闭包的结构打印出来,结构如下:
可以看到,ClosureData 中 object 存储的地址直接就是 person 内存地址。因为在初始化 SHPerson 的时候已经开辟了堆空间,没有必要再开辟一个堆空间来捕获这个 person,所以直接把 person 的内存地址直接放到 ClosureData 中,这样可以避免不必要的内存开销。
5. 捕获多个值
5.1. 分析 getFn 函数
如果捕获多个值,闭包的结构还和第 3 点还原出来的一样吗,代码如下:
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num1 = 0
var num2 = 0
func plus(_ i: Int) -> Int {
num1 += i
num2 += (num1 + 1)
return num2
}
return plus
}
let fn = getFn()
我们将当前的 main.swift 文件编译成 main.ll 文件,编译成功之后我们直接看 getFn 函数的实现:
代码比较长,我只截了关键的部分。可以看到,在捕获多个值后,相对应的也多次调用了 swift_allocObject 方法。注意看,第一次和第二次调用 swift_allocObject 都是为了存储 num1 和 num2 的值。
比较有意思的是第三次调用 swift_allocObject,返回的实例被强制转换成了一个结构体指针:
<{ %swift.refcounted, %swift.refcounted*, %swift.refcounted* }>*
注意看 getelementptr,第三次调用 swift_allocObject 后有两次 getelementptr。这两次的 getelementptr 在干什么,在把前两次 swift_allocObject 的结构体存储到 %13 这个结构体。
5.2. 还原闭包的结构
根据以上分析,我们将这个闭包的结构还原出来后如下:
struct ClosureData<MutiValue>{
/// 函数地址
var ptr: UnsafeRawPointer
/// 存储捕获堆空间地址的值
var object: UnsafePointer<MutiValue>
}
struct MutiValue<T1,T2>{
var object: HeapObject
var value: UnsafePointer<Box<T1>>
var value1: UnsafePointer<Box<T2>>
}
struct Box<T>{
var object: HeapObject
var value: T
}
struct HeapObject {
var matedata: UnsafeRawPointer
var refcount: Int
}
测试代码如下:
//用结构体包裹一下闭包方便作指针的转换
struct ClosureStruct {
var closure :(Int) -> Int
}
var fn = ClosureStruct(closure: getFn())
fn.closure(10)
//fn 初始化一个ClosureStruct类型指针
let ptr = UnsafeMutablePointer<ClosureStruct>.allocate(capacity: 1)
ptr.initialize(to: fn)
//内存重新绑定为 ClosureData<Box<Int>>
let ctx = ptr.withMemoryRebound(to: ClosureData<MutiValue<Int, Int>>.self, capacity: 1){
$0.pointee
}
print("闭包的调用地址:",ctx.ptr)
print("堆空间地址:",ctx.object)
print("堆空间存储的值", ctx.object.pointee.value.pointee.value, ctx.object.pointee.value1.pointee.value)
ptr.deinitialize(count: 1)
ptr.deallocate()
打印结果:
闭包的调用地址: 0x0000000100002840
堆空间地址: 0x000000010111f400
堆空间存储的值 10 11
5.3.捕获单个值和多个值的闭包结构总结
根据以上的分析,捕获单个值和多个值的区别就在于:
-
单个值中,ClosureData 内存储的堆空间地址直接就是这个值所在的堆空间。
-
而对于捕获多个值,ClosureData 内存储的堆空间地址会变成一个可以存储很多个捕获值的结构。
总的来时捕获多个值比捕获单个值多了一层包装,那么总结的代码结构如下:
// 捕获单个值的 ClosureData
struct ClosureData<Box> {
/// 函数地址
var ptr: UnsafeRawPointer
/// 存储捕获堆空间地址的值
var object: UnsafePointer<Box>
}
// 捕获多个值的 ClosureData
struct ClosureData<MutiValue>{
/// 函数地址
var ptr: UnsafeRawPointer
/// 存储捕获堆空间地址的值
var object: UnsafePointer<MutiValue>
}
struct MutiValue<T1, T2, ......>{
var object: HeapObject
var value: UnsafePointer<Box<T1>>
var value1: UnsafePointer<Box<T2>>
// 更多的 value
......
}
struct Box<T>{
var object: HeapObject
var value: T
}
struct HeapObject {
var matedata: UnsafeRawPointer
var refcount: Int
}
五、逃逸闭包
当闭包作为一个实际参数传递给一个函数的时候,并且是在函数返回之后调用,我们就说这个闭包逃逸了。当我们声明一个接受闭包作为形式参数的函数时,可以在形式参数前写 @escaping 来明确闭包是允许逃逸的。
- 当闭包被当作属性存储,导致函数完成时闭包生命周期被延长。
-
当闭包异步执行,导致函数完成时闭包生命周期被延长。
-
可选类型的闭包默认是逃逸闭包。
以下这种闭包其实也是逃逸,对于编译器来说,把一个闭包赋值给了一个变量,编译器认为这个闭包可能会在其他地方去执行。
func test() -> Int{
var age = 10
let completeHandler = {
age += 10
}
completeHandler()
return age
}
逃逸闭包所需的条件:
-
作为函数的参数传递。
-
当前闭包在函数内部异步执行或者被存储。
-
函数结束,闭包被调用,闭包的生命周期未结束。
六、自动闭包
@autoclosure 是一种自动创建的闭包,用于将参数包装成闭包。这种闭包不接受任何参数,当它被调用的时候,会返回传入的值。这种便利语法让你在调用的时候能够省略闭包的花括号。
什么意思呢,我们来看下面的代码:
// 如果第1个数大于0,返回第一个数。否则返回第2个数
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
return v1 > 0 ? v1 : v2
}
print(getFirstPositive(10, 20)) // 10
print(getFirstPositive(-2, 20)) // 20
print(getFirstPositive(0, -4)) // -4
用了一个三目运算符,判断返回的是 v1 还是 v2,接下来我添加一个测试函数,如下:
func getNum() -> Int {
print("getNum")
let a = 10
let b = 20
return a + b
}
print(getFirstPositive(10, getNum()))
打印结果:
getNum
10
注意看,我传一个 10 给 getFirstPositive 方法,确实也返回一个 10 了,可是却打印出了 getNum,但其实判断 v1 > 0 并不需要调用 getNum 函数,但编译器还是执行了。
这个时候我们可以把 v2 变成一个函数,也就是我们传一个函数进去,代码如下:
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
print(getFirstPositive(10, {
print("test")
return 30 }))
打印结果:
10
可以看到,调用 getFirstPositive 后并没有打印出 test,这就可以去优化我们的代码,避免不必要的代码执行。但是如果我们的代码比较简单,就像上面的例子,可以这么写:
print(getFirstPositive(10, {20}))
可是这种写法就比较麻烦,每次都要加上一对花括号,那我们就可以用自动闭包去表达,如何使用自动闭包呢,代码如下:
func getFirstPositive(_ v1: Int, _ v2:@autoclosure () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
print(getFirstPositive(10, 20))
在 v2: 后面加上了 @autoclosure 就形成了自动闭包,我们在使用的时候也可以省略掉花括号。
-
@autoclosure 只支持 () -> T 格式的参数。
-
@autoclosure 并非只支持最后一个参数。
-
空合并运算符(??)使用了 @autoclosure 技术。
-
有 @autoclosure,无 @autoclosure 构成函数重载。