Skip to content

22-闭包

介绍

闭包在仓颉编程中属于比较抽象的知识点。我们先来理解闭包的作用,然后再来学习的它的语法。

闭包的出现可以让我们实现隐藏私有变量或者缓存数据状态的作用。另外它的出现也可以让我们的某些代码得到简化。

因此闭包的出现是让某种场景下编程体验更好,而不是非用闭包不可。

闭包的定义

一个函数或 lambda 从定义它的静态作用域中捕获了变量,函数或 lambda 和捕获的变量一起被称为一个闭包,这样即使脱离了闭包定义

所在的作用域,闭包也能正常运行。

示例代码

javascript
class C {
    public var num: Int64 = 0
}

func returnIncrementer(){
    let c: C = C()

    func incrementer() {
        c.num++
        println( c.num)
    }

    incrementer
}

main() {
    let f = returnIncrementer()
    f() // 1 
    f() // 2
    f() // 3 
}

“一个函数或 lambda 从定义它的静态作用域中捕获了变量,函数或 lambda 和捕获的变量一起被称为一个闭包,这样即使脱离了闭包定义

所在的作用域,闭包也能正常运行。”

  1. incrementer 是捕获了变量的那个函数
  2. C 是被捕获的变量,也是被缓存、被隐藏的变量
  3. f 只是调用闭包中函数的体现
  4. 最终,当我们多次调用 函数f时,内部被捕获的变量,就会一值增加。

以上就是闭包中最为精简的理解了。

接下来的内容无非就是闭包的写法的限制和不同的用法。

变量捕获

一个函数或 lambda 从定义它的静态作用域中捕获了变量,函数或 lambda 和捕获的变量一起被称为一个闭包,这样即使脱离了闭包定义

所在的作用域,闭包也能正常运行。

函数或 lambda 的定义中对于以下几种变量的访问,称为变量捕获:

  • 函数的参数缺省值中访问了本函数之外定义的局部变量;
  • 函数或 lambda 内访问了本函数或本 lambda 之外定义的局部变量;
  • class/struct 内定义的不是成员函数的函数或 lambda 访问了实例成员变量或 this

以下情形的变量访问不是变量捕获:

  • 对定义在本函数或本 lambda 内的局部变量的访问;
  • 对本函数或本 lambda 的形参的访问;
  • 对全局变量和静态成员变量的访问;
  • 对实例成员变量在实例成员函数或属性中的访问。由于实例成员函数或属性将 this 作为参数传入,在实例成员函数或属性内通过 this 访问所有实例成员变量。

变量的捕获发生在闭包定义时,因此变量捕获有以下规则:

  • 被捕获的变量必须在闭包定义时可见,否则编译报错;
  • 被捕获的变量必须在闭包定义时已经完成初始化,否则编译报错。

示例代码

示例 1:闭包 add,捕获了 let 声明的局部变量 num,之后通过返回值返回到 num 定义的作用域之外,调用 add 时仍可正常访问 num

javascript
func returnAddNum(): (Int64) -> Int64 {
    let num: Int64 = 10

    func add(a: Int64) {
        return a + num
    }
    add
}

main() {
    let f = returnAddNum()
    println(f(10))
}

程序输出的结果为:

text
20

示例 2:捕获的变量必须在闭包定义时可见。

javascript
func f() {
    let x = 99
    func f1() {
        println(x)
    }
    let f2 = { =>
        println(y)      // Error, cannot capture 'y' which is not defined yet
    }
    let y = 88
    f1()          // Print 99.
    f2()
}

示例 3:捕获的变量必须在闭包定义前完成初始化。

javascript
func f() {
    let x: Int64
    func f1() {
        println(x)    // Error, x is not initialized yet.
    }
    x = 99
    f1()
}

如果捕获的变量是引用类型,可修改其可变实例成员变量的值。

javascript
class C {
    public var num: Int64 = 0
}

func returnIncrementer(): () -> Unit {
    let c: C = C()

    func incrementer() {
        c.num++
    }

    incrementer
}

main() {
    let f = returnIncrementer()
    f() // c.num increases by 1
}

为了防止捕获了 var 声明变量的闭包逃逸,这类闭包只能被调用,不能作为一等公民使用,包括不能赋值给变量,不能作为实参或返回值使用,不能直接将闭包的名字作为表达式使用。

javascript
func f() {
    var x = 1
    let y = 2

    func g() {
        println(x)  // OK, captured a mutable variable.
    }
    let b = g  // Error, g cannot be assigned to a variable

    g  // Error, g cannot be used as an expression
    g()  // OK, g can be invoked

    g  // Error, g cannot be used as a return value.
}

需要注意的是,捕获具有传递性,如果一个函数 f 调用了捕获 var 变量的函数 g,且 g 捕获的 var 变量不在函数 f 内定义,那么函数 f 同样捕获了 var 变量,此时,f 也不能作为一等公民使用。

以下示例中,g 捕获了 var 声明的变量 xf 调用了 g,且 g 捕获的 x 不在 f 内定义,f 同样不能作为一等公民使用:

javascript
func h(){
    var x = 1

    func g() {  x }   // captured a mutable variable

    func f() {
        g()      // invoked g
    }
    return f // Error
}

以下示例中,g 捕获了 var 声明的变量 xf 调用了 g。但 g 捕获的 xf 内定义,f 没有捕获其它 var 声明的变量。因此,f 仍作为一等公民使用:

javascript
func h(){
    func f() {
        var x = 1
        func g() { x }   // captured a mutable variable

        g()
    }
    return f // Ok
}

静态成员变量和全局变量的访问,不属于变量捕获,因此访问了 var 修饰的全局变量、静态成员变量的函数或 lambda 仍可作为一等公民使用。

javascript
class C {
    static public var a: Int32 = 0
    static public func foo() {
        a++       // OK
        return a
    }
}

var globalV1 = 0

func countGlobalV1() {
    globalV1++
    C.a = 99
    let g = C.foo  // OK
}

func g(){
    let f = countGlobalV1 // OK
    f()
}

Released under the MIT License.