swift 中的闭包捕获

关于闭包捕获相关的问题在官方文档上并没有写在一起, 系统地整理一下, 复习了一遍.


目录

闭包表达式的基本形式

 1
 2
 3
{ [copyItem = item, anotherItem, unkonwn self] (paraA: Int, paraB: String) -> String in
    // some code
}

随便写了一个, 一个完整没有简写的闭包包括捕获列表, 参数类型和返回值, 参数和返回值与普通函数差不多, 就不提了. 捕获列表中可以为被捕获的值起一个别名, 也可以直接直接使用原来的名称(但实际上是闭包外变量的一个拷贝), 为了防止循环引用, 可以在被捕获的引用变量前加weak, unknown等修饰.

闭包中默认捕获的是变量的引用

为了下面方便演示, 首先定义下面这个延迟函数:

 1
 2
 3
 4
 5
 6
func delay(_ time: Int = 2, @escaping closure: () -> ()) {
    DispatchQueue.main.asyncAfter(deadline: .now() + DispatchTimeInterval.seconds(time)) {
        print("colsure start execute")
        colsure()
    }
}

这个函数要把传入的闭包延迟两秒执行, 因为在函数执行后才执行闭包, 所以使用@escaping来修饰闭包. emmmm没搞清 playground 里怎么弄延迟, 所以还是用iOS程序来运行吧.

被捕获的是值类型

 1
 2
 3
 4
 5
var a = 0
delay {
    print("a = \(a)")
}
a = 10

输出为:

 1
 2
closure start execute
a = 10

可以发现闭包并没有把a的值复制下来, 而是直接捕获了它的引用, 在闭包执行前修改a, 执行闭包的时候通过引用找到现在a的值, 打印10.

如果使用捕获列表, 那么就会在闭包内对捕获列表中的值进行复制, 闭包内外的变量此时不是同一个变量了.

 1
 2
 3
 4
 5
var a = 0
delay { [copyA = a] in
    print("a = \(copyA)")
}
a = 10

输出为:

 1
 2
closure start execute
a = 0

等同于下面这么写

 1
 2
 3
 4
 5
 6
var a = 0
var copyA = a // 注意这个变量只可以在闭包内访问
delay {
    print("a = \(copyA)")
}
a = 10

被捕获的是引用类型

因为闭包内捕获的是引用, 所以被捕获值本是是引用类型而言, 捕获的并不是引用的值, 而是引用类型本身的一个引用, 有点像C语言中的双重指针.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Person {
    var name: String

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(self.name) is dead")
    }
}

var b = Person(name: "B")
delay {
    print("the name of b is \(b.name)")
}
b = Person(name: "C")

输出为:

 1
 2
 3
 4
B is dead
closure start execute
the name of b = C
C is dead

和上面值类型的例子差不多, 因为捕获的是引用类型本身的引用, 并没有持有类本身, 所以当给b赋给其他值的时候, person B引用计数为0, 进行了析构. 闭包调用的时候, 通过变量b的引用找到了b本身, 再跟据b找到了person C实例, 输出结果.

如果使用捕获列表, 情况就不一样了, 因为捕获列表是复制一份值到闭包内, 所以闭包内的复制变量会持有类实例的强引用.

 1
 2
 3
 4
 5
var b = Person(name: "B")
delay { [copyB = b]
    print("then name of b is \(b.name")
}
b = Person(name: "C")

输出为:

 1
 2
 3
 4
closure start execute
the name of b is B
B is dead
C is dead

因为闭包里的copyB就是b的复制, 而不是值引用, 所以同样可以对类实例保留强引用.

引用是可以修改外部变量的

最后, 因为捕获的是引用, 所以如果不是使用捕获列表复制变量的话, 闭包内部是可以修改外部被捕获的变量的

 1
 2
 3
 4
 5
var a = 0
delay {
    a = 10
}
print("a = \(a)")

输出为:

 1
 2
closure start execute
a = 10

闭包引起的循环引用

闭包本身是引用类型

因为函数, 或者闭包在 swift 中是一等值, 所以闭包是可以以赋值的形式传来传去的, 在传递的过程中, 闭包本身没有进行复制, 传值传的只是闭包自身的引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func makeIncrease(_ num: Int) -> () -> Int {
    var base = 0
    let closure =  {
        base += num
        return base
    }
    return closure
}

let increase = makeIncrease(10)
increase()
// 10
increase()
// 20
let anotherIncrease = increase
anotherIncrease()
// 30

函数makeIncrease返回一个闭包, 每执行一个这个闭包, 返回值增加num. 可以看出当把闭包由increase传给anotherIncrease时, 还是同一个闭包, 说明传递的只是闭包的引用.

闭包对引用类型本身的引用是怎么释放的

说起来比较绕口, 还是用上面闭包捕获引用类型的例子来说明, 为了更清晰地看出引用类型什么时候脱离作用域进行析构, 我们把代码放在函数里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
}

func demo() {
    var b = Person(name: "B")
    delay {
        print("the name of b is \(b.name)")
    }
    b = Person(name: "C")
}

demo()

输出为:

 1
 2
 3
 4
B is dead
closure start execute
the name of b is C
C is dead

开始的时候并没有说最下面的一行输出是怎么回事, 当离开demo的作用域时(此时闭包还没有执行), person C并没有析构, 因为引用类型变量b还在被闭包引用着, 而b保留着对person C的引用, 所以直到闭包运行结束之后, 变量b脱离作用域, person C引用计数为0, 进行析构.

闭包的引用循环

因为闭包本身是引用类型, 可以被类或其他闭包引用; 而闭包本身可以引用类或者其他闭包, 这就可以会构成引用循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
    var name: String

    lazy var introduce: () -> String = {
        let str = "my name is \(self.name)"
        return str
    }

    init(name: String) {
        self.name = name
    }
}

var a = Person(name: "A")
a.introduce()
// my name is A
a.introduce = {
    let str = "I am a person"
}
a.introduce()
// I am a person

introduce是类Person的一个变量, 它是一个闭包, 将它声明为懒加载是因为它引用了self, 而懒加载可以保证只有当self初始化结束, 并且self存在的时候才可以调用这个变量.

可以看出, 闭包中引用了self, 也就是说闭包有对self的强引用; 而闭包本身又是self的一个变量, 即self又强引用了闭包, 这就造成了循环引用, 可以使用unknown, weak来修饰这个闭包变量, 或者, 因为我们是在讲闭包, 所以可以在捕获列表中使用unknown, weak来避免循环引用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Person {
    var name: String

    lazy var introduce: () -> String = { [unknown self] in
        let str = "my name is \(self.name)"
    }

    init(name: String) {
        self.name = name
    }
}

因为类实例析构的时候闭包肯定也不在了, 而使用闭包的时候类一定还在, 所以可以保证在使用期间self不会为nil, 在捕获进闭包时使用unknown修饰self就可以了.

总结

看这些例子还是有点绕, 其实总结起来注意几点就好:

  • 闭包默认捕获的是值的引用
  • 如果是引用类型的值, 闭包捕获的是这个引用的引用
  • 捕获列表里捕获的是复制进来的值, 而不是引用
  • 闭包本身也是引用类型

emmmm, 上次更新服务器系统时忘了备份数据库, 再加上评论比较少, 暂时关闭评论功能