闭包

作者: 曹来东 | 来源:发表于2018-10-09 15:53 被阅读8次

    闭包这个概念起源于将函数作为头等类的函数式编程语言。闭包所实现的是一种词法作用域中的名字绑定。也就是说,闭包其实是一个将函数与一个执行环境存放在一起的一条记录。当闭包被创建时,它会将它所在的函数中的局部对象与它自己的执行环境中的对象名做一个值或引用的关联映射。这就是所谓的名字绑定(name binding)。所以一个闭包与普通的函数不同,闭包允许自己访问它所在函数中的局部对象的值或引用。如果闭包访问了它所在函数的局部对象,那么我们称它捕获(capture)了该局部对象。由于闭包有其自己的执行环境,我们也称之为执行上下文,因此即便当它所在的函数被返回之后,该闭包依然能正常调用。
    Apple官方编程指南中将闭包定义为:可以在你代码中传递并使用的自包含的功能块。它类似于C与Objective-C中的Blocks以及其他编程语言中的lambda。
    但这里要说明的是,闭包是一种lambda表达式,但不是所有的lambda表达式都能作为闭包!一个lambda要作为闭包,必须满足两个条件,一个就是它能捕获它所在函数的局部对象(按值或引用进行传递),另一个就是要有自己的执行上下文,使得它所在的函数返回之后依然能正常调用执行。因此,C++中的lambda严格意义上不是闭包,由于它与当前函数共享了执行上下文,所以当该函数返回之后,该lambda的上下文也会受到破坏。而Java中的lambda表达式尽管也能捕获它所在方法的局部对象以及也拥有自己的上下文,但它只能捕获常量对象,并且限制约束也比较多,所以也称不上真正意义上的闭包。而C与Objective-C的Blocks则可以算属于闭包,尽管我们要返回一个Block时需要使用Block_copy接口来动态创建其上下文。
    我们之前提到的捕获外部函数局部对象的嵌套函数属于命名闭包,它也是个闭包。不过Swift编程语言中将不捕获任何局部对象的全局函数也作为闭包,在概念上有些不妥,尽管我们也表示,Swift将函数类型全都统一起来确实是个很不错的实现。但是在概念上,将函数、闭包与方法三者独立更好些,尽管编程语言可以实现统一三者的一个抽象类型。不过我们仍然可以区分出闭包与一般函数。

    闭包的定义与调用

    在Swift编程语言中,一个闭包又称为闭包表达式(closure expression),其基本语法形式如下:

    _ = { (param1: Int, param2: Float, param3: Void) -> return_type in
    // 闭包执行代码
    /* ... */
    }
    

    这里我们可以看到,Swift中的闭包先由一对花括号引出,就好像C语言中的一个语句块。然后跟着闭包的形参列表,由圆括号围着,这跟函数的形参列表的表示法一样。随后跟闭包的返回类型,这也跟函数的返回类型表示法一样。最后用 in 关键字来引出闭包的执行代码。
    Swift中的闭包表达式不能单独出现,因为稍后会介绍Swift具有尾随闭包这一语法糖,因此闭包表达式如果不作为某个函数的实参或没有放在 = 赋值操作数的后面,那么它可能会被编译器识别为一个尾随闭包而引发错误。所以我们如果要单独引出一个闭包表达式然后直接对它进行调用的话应该像上述闭包表达式语法说明的代码一样,在前面用通配符再加赋值操作符来表达。我们先看以下闭包表达式的定义。

    // 这里定义了一个简单闭包,
    // 它没有任何形参,返回类型为Void
    _ = {
    () -> Void in
    print("This is a simple closure")
    }
     
    // 这里闭包的含有一个形参,类型为Int,
    // 返回类型为Void
    _ = {
    (a: Int) -> Void in
    print("a = \(a)")
    }
     
    // 这里闭包有两个形参,类型均为Int,
    // 返回类型为 () -> Void 函数类型
    _ = {
    (a: Int, b: Int) -> () -> Void in
     
    let sum = a + b
    var x = sum * 2
     
    // 这里在一个闭包内定义了一个嵌套闭包,
    // 其参数列表为空,返回类型为Void
    return {
    () -> Void in
    // 在此嵌套闭包中捕获了其外部闭包的sum常量
    // 以及x变量
    print("sum = \(sum)")
    x += sum
    }
    }
    

    在上述代码中,我们可以看到闭包表达式的定义方式,然后看到闭包的类型也属于函数类型,其参数、返回类型与函数表达的形式一样,并且也能在一个闭包内再定义一个嵌套闭包。这里我们要注意的是,对于一个闭包而言,它没有所谓的实参标签!因为闭包本身就是一个匿名函数,它连函数名都没有所以也就没有所谓的函数签名,因此它自然就不需要实参标签了,它完全是匿名的!
    然后我们再看闭包的调用。对闭包的调用与函数调用一样,我们可以直接在闭包表达式后面跟函数调用操作符,形成一个完整的函数调用表达式。

    // 以下表达式直接对闭包进行调用
    // 这里要注意的是,函数调用操作符 ()
    // 与闭包表达式之间不能有换行符
    _ = {
    () -> Void in
    print("This is a simple closure")
    } ()
     
    // 因为换行符与分号一样,属于一条语句的结束符,
    // 所以要是存在换行就会变成:
    _ = {
    () -> Void in
    print("This is a simple closure")
    };
    // 注意!这里的()是一个空元组表达式
    // 也就是一条空语句
    () ;
    // 这里直接调用一个带有一个形参的闭包
    _ = {
    (a: Int) -> Void in
    print("a = \(a)")
    } (10) // 我们看到,这里没有实参标签
     
    // 这里声明了一个函数类型的引用指向闭包
    // 其类型为:(Int, Int) -> () -> Void
    let ref = {
    (a: Int, b: Int) -> () -> Void in
     
    let sum = a + b
    var x = sum * 2
     
    // 这里在一个闭包内定义了一个嵌套闭包,
    // 其参数列表为空,返回类型为Void
    return {
    () -> Void in
    // 在此嵌套闭包中捕获了其外部闭包的sum常量
    // 以及x变量
    print("x = \(x), sum = \(sum)")
    x += sum
    }
    }
     
    // 这里声明了一个函数类型的引用,
    // 指向ref所引用的闭包所返回的嵌套闭包。
    let inner = ref(10, 20)
     
    // 调用嵌套闭包
    // 输出:x = 60, sum = 30
    inner()
     
    // 这次调用输出:x = 90, sum = 30
    inner()
     
    // 我们可以直接对ref做两次调用
    // 输出:x = 600, sum = 300
    ref(100, 200)()
     
    // 当一个函数类型中返回类型也是一个函数类型时,
    // 我们可以用圆括号做显式隔离。
    let ref2: (Int, Int) -> ( () -> Void ) = ref
     
    // 由于 -> 作为返回类型时所采用的是右结合方式,
    // 因此(Int, Int) -> ( () -> Void )
    // 与(Int, Int) -> () -> Void 是等价的。
    // 这里输出:x = 60, sum = 30
    ref2(10, 20)()
    
    

    对于上述代码,我们这里要提到的一点是,对于一条函数调用表达式而言,表示函数标志的表达式与函数调用操作符之间不能用换行符分隔,否则它们将会被视作为两条独立的语句,函数调用操作符将会被视为空元组或一般的圆括号表达式。我们这里再举些例子说明一下。

    func foo() {
    print("foo")
    }
     
    // 这里是两条语句,
    // 前一条是_ = foo,
    // 后一条则是()作为空元组表达式语句
    _ = foo
    ()
     
    func foo(_ a: Int) {
    print("a = \(a)")
    }
     
    // 这里也是两条语句,
    // 前一条是_ = foo(_:),
    // 后一条是(100)作为一个圆括号表达式语句
    _ = foo(_:)
    (100)
     func boo() -> (Int, Int) -> Void {
    return { (a: Int, b: Int) -> Void in
    print("value = \(a + b)")
    }
    }
     
    // 这里也是两条语句,
    // 前一条是_ = boo()
    // 后一条是(1, 2)一个元组字面量构成的表达式语句
    _ = boo()
    (1, 2)
    /*
    boo()(1, 2)
    //等价
    let ref = boo()
    
    ref(1,2)
    */
    

    闭包表达式的简略表达

    先看一下缺省返回类型的表达方式。如果Swift编译器能根据当前上下文推断出当前闭包的返回类型,那么返回类型可缺省。这里各位要注意的是,一个函数如果其返回类型缺省,那么其返回类型即被确定为 Void。而闭包的返回类型一旦缺省,那么其返回类型则未必是 Void,也可能是其他类型。我们先看以下例子:

    let ref = { (a: Int, b: Int) in
    return a + b
    }
     
    print("value = \(ref(1, 2))")
    

    上述闭包的类型为 (Int, Int) -> Int,尽管返回类型缺省了,但Swift编译器仍然能够通过这里 return 语句后面的表达式推导出返回类型为 Int。
    Swift不仅能推导闭包的返回类型,而且还能推导闭包形参的类型,因此闭包形参的类型也可缺省,并且此时圆括号也可缺省。如果闭包形参列表的圆括号缺省,那么每个形参的类型都必须缺省。我们看以下例子:

    // 由于这里显式注明了ref函数引用对象的类型,
    // 因此 = 后面闭包表达式的形参类型可缺省
    var ref: (Int, Int) -> Int = { a, b in
    return a + b
    }
     
    // 这里闭包的形参类型缺省,
    // 不过添加了返回类型
    ref = { a, b -> Int in
    return a + b
    }
     
    // 这里闭包形参a的类型缺省,
    // 但b的类型显式注明了,
    // 因此这里形参列表的圆括号不能缺省
    ref = { (a, b: Int) in return a + b }
     
    // 这里闭包表达式中每个形参的类型缺省,
    // 但仍然保留了圆括号,这也是OK的
    ref = { (a, b) in return a + b }
     
    print("value = \(ref(1, 2))")
    

    上述代码展示了闭包表达式形参类型缺省情况。而如果闭包表达式中其执行语句只由一个单独的 return 语句构成,那么关键字 return 也可缺省。我们看以下代码:

    // 由于这里in后面就一条表达式语句,
    // 因此连return语句都可省
    var ref: (Int, Int) -> Int = { a, b in a + b }
    print("value = \(ref(1, 2))")
     
    // 我们这里再次利用元组字面量表达式的奇技淫巧
    // 使得我们这里既可缺省return语句,
    // 又能实现执行两个表达式的功能
    let ref2 = { (a: Int) -> Int in
    (print("a = \(a)"), a + 1).1
    }
     
    // 这里输出:
    // a = 10
    // value = 11
    print("value = \(ref2(10))")
    

    我们下面介绍闭包表达式简略表达的究极情况!一个闭包表达式如果确定其每个形参的类型以及返回类型,那么该闭包表达式中连 in 关键字都能缺省,此时它看上去就如同一个C语言的语句块。而当我们要引用其形参时使用 $ 符号加上形参索引:第一个形参索引为0,第二个形参索引为1,后续的以此类推。下面我们看一些例子:

    // 这里使用了闭包表达式的究极简略表达形式,
    // 其功能也很容看出,
    // 就是直接返回两个形参的和
    var ref: (Int, Int) -> Int = { $0 + $1 }
    print("value = \(ref(1, 2))")
     
    // 尽管这里ref2没有指定类型,
    // 并且闭包表达式也没有参数列表以及返回类型,
    // 但Swift编译器仍然能推导出该闭包的类型为:
    // () -> Void
    let ref2 = {
    print("hello")
    print("world")
    }
    ref2()
     
    // 由于此闭包表达式中的执行代码由多条语句构成,
    // 因此最后的return语句不可省,
    // 但仍然将参数列表以及返回类型全都缺省了
    ref = {
    print("1st param = \($0)")
    print("2nd param = \($1)")
    return $0 * $1
    }
    print("mul result: \(ref(2, 3))")
     
    // 尽管这里闭包表达式的执行语句有多条,
    // 但由于其返回类型为Void,
    // 因此return语句自然即可默认缺省
    let ref3: (Int) -> Void = {
    print("param value = \($0)")
    let a = $0 * $0
    print("a = \(a)")
    }
    ref3(5)
    

    尾随闭包

    如果一个闭包表达式作为函数调用的最后一个实参,那么我们可以采用尾随闭包(trailing closures)语法糖。当我们采用尾随闭包语法时,如果该函数最后一个形参带有实参标签,那么该实参标签也被省去。下面我们举一些例子。

    // 我们这里定义了一个函数,它具有两个形参。
    // 第一个形参类型为Int,
    // 第二个形参类型为 (Int) -> Void 的函数类型
    func foo(_ a: Int, callback: (Int) -> Void) {
    callback(a)
    }
     
    // 我们一般这么调用foo函数
    foo(10, callback: { print("param = \($0)") })
     
    // 如果我们使用尾随闭包语法,
    // 那么可以这么调用foo函数,
    // 此时callback实参标签也必须缺省
    foo(10) {
    print("param = \($0)")
    }
    // 这里定义了另一个foo,它具有三个形参。
    // 前两个形参类型为Int,
    // 第三个形参类型为 ((Int, Int) -> Int)? 一个Optional的函数类型
    func foo(_ a: Int, _ b: Int, callback: ((Int, Int) -> Int)?) {
    if let callback = callback {
    let value = callback(a, b)
    print("value = \(value)")
    }
    }
     
    // 我们使用尾随闭包语法来调用foo函数。
    // 调用之后会输出:value = 34
    foo(3, 5) {
    // 我们这里对闭包表达式提供了完整的函数类型
    (a: Int, b: Int) -> Int in
    return a * a + b * b
    }
     
    func foo(callback: () -> Void) {
    callback()
    }
     
    // 如果一个函数只有一个形参,并且为函数类型,
    // 那么我们在使用尾随闭包时连函数调用操作符也可缺省
    foo { print("This is a closure") }
    

    我们看到,如果在一个函数调用最后使用了尾随闭包,这个函数调用看上去十分像一个函数定义。所以我们在读代码的时候需要看所调函数之前有没有关键字 func,如果有 func 说明这是一个函数定义,没有则说明是函数调用。
    在Swift编程语言中并不是所有情况都能使用尾随闭包语法糖。如果一个函数调用表达式作为一条if、while等控制流语句的表达式时,那么我们就不能直接使用尾随闭包语法,否则会与if、while等控制流语句后面的语句块相冲突。此时,我们可以将整个函数调用表达式使用圆括号进行分隔,以辅助指示编译器区分函数调用表达式与控制流语句块。我们看一些例子。

    func foo(_ a: Int, callback: (Int) -> Void) -> Bool {
    callback(a)
    return a > 0
    }
     
    // 这里foo函数调用表达式作为if语句的控制表达式,
    // 因此不能直接使用尾随闭包语法,
    // 否则就会跟if后面的{ }语句块相冲突。
    // 所以这里将整个对foo函数调用的表达式通过圆括号进行分隔
    if (foo(10) { print("param = \($0)") }) {
    print("OK!")
    }
    repeat {
    print("a loop")
    }
    // 这里可以直接使用尾随闭包,
    // 由于这里尾随闭包的{ }不会与控制流语句的语句块相冲突。
    // 另外,由于这里闭包表达式中没有引用形参,
    // 因此形参标识符可省
    while foo(-1) { (Int) -> Void in
    print("OK")
    }
     
    // 我们这里直接将一个闭包调用表达式作为for语句的控制表达式,
    // 最后输出四行:for loop
    for _ in ({ return $0 ..< $1 }(1, 5)) {
    print("for loop")
    }
     
    func foo(_ a: Int, _ b: Int, closure: (Int, Int) -> CountableRange<Int>) -> CountableRange<Int> {
    return closure(a, b)
    }
     
    // 这里使用了函数调用表达式作为for语句的控制表达式,
    // 同时也使用了尾随闭包语法。
    // 最后输出四行:foo
    for _ in (foo(0, 4) { return $0 ..< $1 }) {
    print("foo")
    }
    

    捕获局部对象与闭包的执行上下文

    闭包可以捕获其所在函数的局部对象,并且对它所捕获的变量进行修改。Swift以引用的方式捕获所有局部对象,无论该对象是常量还是变量。尽管Swift实现可能会对常量的捕获或者没有在闭包中修改的局部对象的捕获采用按值拷贝的方式做优化,但在概念上我们仍然要把所有对外部函数局部对象的捕获视为以引用的方式进行捕获。因为采用引用的方式进行捕获能够使得当前闭包所在的函数在调用返回后,该闭包所捕获的所有对象在其自己的上下文中得以正确保存。
    既然这里提到了引用,那么下面我们就来证明闭包所捕获的对象是按引用进行捕获的,同时闭包本身是一个引用对象。

    // 我们先证明数组是值类型
    var a = [1, 2, 3]
     
    // 这里其实是将数组a的元素拷贝给数组b
    var b = a
     
    a[0] += 10
     
    // 这里输出:a[0] = 11, b[0] = 1
    // 足以说明了数组a与数组b具有相互独立的存储空间,
    // 所以它们所指代的是两个不同的对象
    print("a[0] = \(a[0]), b[0] = \(b[0])")
     
    // 这里我们来证明
    // 闭包是以引用形式捕获其外部函数的局部对象的
    func foo() {
    var a = 10
     
    let closure = {
    // 在这个闭包中没有对它所捕获的局部对象a进行修改
    print("a = \(a)")
    }
     
    // 我们先调用一次闭包,输出:a = 10
    closure()
     
    // 我们对foo的局部对象a做次修改
    a += 10
     
    // 再次调用闭包,输出:a = 20
    // 说明a的值也相应更改了,
    // 因此闭包所捕获的a不是按值拷贝的形式,
    // 而是按引用的方式进行的
    closure()
    }
     
    foo()
    // 下面我们来证明闭包对象是引用类型
    func boo() -> () -> Void {
    var x = 10
     
    return { x += 10
    print("x = \(x)")
    }
    }
     
    var closure = boo()
     
    // 我们再定义了一个ref指向closure
    var ref = closure
     
    // 先调用一次closure
    // 输出:x = 20
    closure()
     
    // 然后我们再调用一次ref
    // 输出:x = 30
    ref()
     
    // 我们再调用一次closure
    // 输出:x = 40
    // 这足以说明ref与closure两者共享一个闭包的执行上下文
    closure()
    // 我们这次让ref指向另一次对boo调用后所返回的闭包对象
    ref = boo()
     
    // 我们先调用一次closure
    // 输出:x = 50
    closure()
     
    // 我们调用再调用一次ref
    // 输出:x = 20
    // 说明此时ref所指向的闭包与closure所指向的闭包具有不同的执行上下文
    ref()
    

    上述代码示例说明了按值拷贝与引用指向之间的区别,并且证实了数组属于值类型,而闭包则属于引用类型,以及闭包所捕获的外部局部对象也是以引用方式进行捕获的。
    我们通过上述代码还能认识到,对于一个函数中的闭包对象而言,每次函数调用都会对该闭包构造一个完全新的对象,包括它拥有自己一个新的执行上下文。对于一般实现而言,定义在函数中的闭包对象会在函数返回前与函数共享执行上下文。而当函数返回后,Swift编译器会自动生成代码以动态创建属于闭包自己的上下文,然后将自己所捕获的其外部函数的局部对象的当前值拷贝到自己的执行上下文中,但对这些值依然以引用的方式进行映射。

    逃逸闭包

    如果我们定义了一个函数,它有一个形参为函数类型,如果在此函数中将通过异步的方式调用该函数引用对象,那么我们需要将此函数类型声明为逃逸的(escaping),以表示该函数类型的形参对象将可能在函数调用操作结束后的某一时刻才会被调用。我们要声明一个函数类型的形参为一个逃逸类型非常简单,只需要在类型前加 @escaping 类型说明符即可。我们先看以下三个例子。

    // 这里声明了一个隐式拆解的Optional函数引用对象ref,
    // 其初始值为空
    var ref: (() -> Void)!
     
    /// 这里定义了一个函数foo,
    /// 它仅有一个形参closure
    /// - parameter closure: 类型为:@escaping () -> Void
    /// 这里如果不加@escaping类型说明符,那么会出现以下编译报错:
    /// 将非逃逸的形参'closure'赋值给一个逃逸的闭包
    func foo(closure: @escaping () -> Void) {
    // 这里仅仅是将形参closure赋值给我们上面定义的ref函数引用对象
    ref = closure
    }
     
    // 调用foo函数,并给出一个简单的闭包
    foo {
    print("Hello, world!")
    }
     
    // 这里是在调用了foo函数之后再通过ref去调用传递给它的闭包对象
    ref()
     
    // 这里定义了另一个foo函数,
    // 形参也是一个函数类型的对象
    // 这里使用了GCD库对形参函数类型对象做异步调用
    func foo(asyncCallback: @escaping () -> Void) {
    // 这里是在主线程上间隔5毫秒之后调用asyncCallback函数对象
    DispatchQueue.main.asyncAfter(deadline: DispatchTime(uptimeNanoseconds: DispatchTime.now().uptimeNanoseconds + UInt64(5_000_000)), execute: asyncCallback)
    }
     
    // 这里再举一个更极端、形象的例子
    func boo(closure: @escaping () -> Void) {
     // 如果closure类型不加@escaping,那么会报以下编译错误:
    // 非逃逸形参'closure'只能被调用
    let f = closure
     
    closure()
     
    f()
    }
     
    boo {
    print("This is only a lambda!")
    }
    

    从以上代码可以看出,如果一个函数的形参闭包对象被另一个函数类型对象所引用,那么就必须为该形参类型添加上 @escaping 类型说明符。言下之意,我们一般在文件作用域以及语句块作用域中所定义的函数类型对象默认即为逃逸闭包,因为像定义在当前所调函数外的对象本来就是独立于该函数调用的。而作为函数形参的函数类型对象则默认为非逃逸的(non-escaping)闭包。

    自动闭包

    自动闭包也是Swift中的一个语法糖。如果一个函数的某个形参为函数类型,并且它不含任何其他形参,那么我们可以将此参数声明为自动闭包(autoclosure)。如果要将一个形参声明为自动闭包,那么在该形参的类型前加 @autoclosure 类型说明符即可。当一个形参作为一个自动闭包时,我们在调用该函数所传入的实参只需要一个表达式即可,Swift编译器会自动将它封装为一个闭包表达式。下面我们先看些例子。

    // 我们定义了一个函数foo,
    // 它有一个形参,类型为 () -> Void
    func foo(closure: () -> Void) {
    closure()
    }
     
    // 这里使用尾随闭包对foo进行调用
    foo {
    print("This is a simple lambda!")
    }
     
    // 这里再次定义了函数foo,
    // 将它的函数类型的形参声明为自动闭包
    func foo(auto closure: @autoclosure () -> Void) {
    closure()
    }
     
    // 我们看到,这里对foo调用时,
    // 形参直接就传入了一个函数调用表达式
    foo(auto: print("This is a simple lambda!"))
     
    func foo() {
    let a = 1, b = -1
     
    /// 这里定义了一个嵌套函数用于比较两个对象的大小。
    /// 其形参类型为 @autoclosure () -> Bool,
    /// 它是一个自动闭包,
    /// isLarger嵌套函数返回Bool类型
    func isLarger(compare: @autoclosure () -> Bool) -> Bool {
    return compare()
    }
     
    // 这里对isLarger进行调用时将 a > b表达式作为一个自动闭包,
    // 这里不需要也不能添加return关键字
    let result = isLarger(compare: a > b)
    print("result: \(result)")
    }
     
    foo()
    

    不过本人这里不推荐各位在制作公共开发的API时使用自动闭包的形式。我们可以在当前文件作用域下自己实现一些私有的函数或方法时用它做一些代码简化,因为自动闭包也存在一些限制:因为自动闭包直接将当前的表达式封装为了一个闭包对象,因此我们对于自动闭包形参就不能单独传一个闭包表达式,因为单独一个闭包表达式不具备执行功能。同时这也意味着我们无法使用尾随闭包的语法糖了。最后,如果我们在一个闭包中想要实现更复杂的功能时,也就是说无法使用用单独一条表达式作为一个闭包的功能时反而会把事情搞得复杂。下面我们看些例子:

    func foo(closure: @autoclosure () -> Void) {
    closure()
    }
     
    // 这里对foo的调用OK!
    // 直接将print("Hello, world!")函数调用表达式作为自动闭包实参
    foo(closure: print("Hello, world!"))
     
    // 这里对foo的调用OK!
    // 将整个 { print("Hello, world!") }() 闭包调用表达式作为自动闭包实参
    foo(closure: { print("Hello, world!") }())
     
    // 这里使用了元组字面量来执行两个函数调用表达式,
    // 最后返回第一个元组元素的对象,即一个空元组
    foo(closure: (print("This is a lambda"), print("Ooops")).0)
     
    // 这里使用尾随闭包,则出现编译报错
    foo {
    print("Hello, world!")
    }
    

    相关文章

      网友评论

          本文标题:闭包

          本文链接:https://www.haomeiwen.com/subject/qkbeaftx.html