仓颉编程语言的闭包

一、仓颉闭包的定义

一个函数或 lambda 从定义它的静态作用域中捕获了变量,函数或 lambda 和捕获的变量一起被称为一个闭包(closure)。

【闭包官方文档

https://cangjie-lang.cn/docs?url=%2F1.0.0%2Fuser_manual%2Fsource_zh_cn%2Ffunction%2Fclosure.html对新手而言,官方文档不太友好】

仓颉语言的闭包是 “函数 /lambda + 捕获的外部变量” 的组合,其目的是允许函数或 lambda 访问定义时的外部变量,即使脱离原始作用域仍能使用这些变量。

仓颉的闭包设计以 “安全性” 为核心,通过严格限制 “捕获可变变量(var变量)的闭包” 的逃逸行为,避免跨作用域的意外修改。

如果学过其它语言的闭包,如从 Python/Java 转向仓颉的闭包,感到不习惯是很正常的 —— 这三种语言对闭包的设计思路差异有点大,细节较多,需要耐心体会,逐步理解。

二、什么是 “变量捕获”?

仓颉中的 “闭包”的核心是 “捕获变量”—— 即函数或 lambda 访问了自身作用域之外的局部变量。但并非所有外部变量访问都算 “捕获”,需明确区分:

属于 “变量捕获” 的情况:

  • 函数参数的默认值访问了函数外的局部变量;
  • 函数或 lambda 内部访问了自身之外定义的局部变量(非全局、非静态);
  • 类 /struct 中,非成员函数(如嵌套函数)访问了实例成员变量或this。

不属于 “变量捕获” 的情况(无需闭包特性):

  • 访问自身内部定义的局部变量;
  • 访问自身的形参;
  • 访问全局变量或静态成员变量(这些变量作用域是全局的,无需 “捕获”);
  • 实例成员函数中访问实例成员变量(通过this传递,不算捕获)。

在仓颉中,“逃逸” 和 “捕获” 是与闭包相关的两个重要概念,它们的含义如下:

逃逸(escape)把闭包带出它的出生地(定义时所在的作用域),让它在外面继续存活。

仓颉额外规则:如果闭包里抓到了任何 var 局部变量,就禁止这种“带出去”的行为。

捕获(capture)闭包在“出生”那一刻,把外层作用域里可见且已初始化的变量

(局部变量、参数默认值里的外部变量、非成员函数里的 this 实例属性等)

打包带走,以便即使离开原作用域仍能读写这些变量。

全局变量、静态成员、实例成员函数里的 this.x 不算捕获。

要点:在仓颉中,闭包定义时捕获外层可见且已初始化的局部变量;若捕获了 var 声明的局部变量(含间接传染),则该闭包禁止逃逸(“禁止逃逸”意味着代码根本过不了编译,不允许),而访问全局、静态成员或实例成员函数里的 this.x 均不算捕获(“不算捕获”意味着代码可以正常编译通过,是允许的)。

区分捕获与非捕获示例

示例1:

var globalVar = 5

// 非捕获场景(自由访问)
class MyClass {
    static var staticMember = 3
    var instanceMember = 4
    
    func method() {
        println(staticMember)        // ✅ 静态成员(非捕获)
        println(instanceMember)      // ✅ 通过 this 访问(非捕获)
        println(globalVar)           // ✅ 全局变量(非捕获)
    }
}

main(): Int64 {
    let obj = MyClass()
    obj.method()
    return 0
}

示例2:

// 全局变量:不算捕获
var globalVar = 100

class MyClass {
    var instanceVar = 200

    // 实例方法:访问实例属性不算捕获(this 是隐式形参)
    func memberFunc() {
        println("实例变量 instanceVar:${instanceVar}")
    }

    func nestedFunc() {
        let localVar = 300      // 外层局部变量

        // innerFunc 捕获了 localVar,但未捕获任何 var
        func innerFunc() {
            println("捕获的外部局部变量 localVar:${localVar}")
            println("未捕获的全局变量 globalVar:${globalVar}")//这里的“未捕获”按仓颉的术语说globalVar 是全局变量“不算捕获”
        }

        // 因为 localVar 是 let,innerFunc 可逃逸
        return innerFunc        // ✅ 编译通过
    }
}

main(): Int64 {
    let obj = MyClass()

    // 1. 成员函数无捕获
    println("--- memberFunc ---")
    obj.memberFunc()

    // 2. nestedFunc 返回捕获 localVar 的闭包
    println("--- nestedFunc ---")
    let f = obj.nestedFunc()  // f 逃逸成功
    f()

    // 3. 全局变量随意访问
    println("--- main 访问 globalVar --- ${globalVar}")

    return 0
}

说明:globalVar 是全局变量,仓颉规则把它视为“不算捕获”,因此即使闭包逃逸出去,也不会触发逃逸检查报错。其中:

println("未捕获的全局变量 globalVar:${globalVar}")

这里的“未捕获”只是按仓颉的术语说“编译器不把它当作对局部变量的捕获”,语法上允许存在,并不是说运行时不能访问。运行时当然能正常打印 globalVar 的值。

输出:

--- memberFunc ---
实例变量 instanceVar:200       
--- nestedFunc ---
捕获的外部局部变量 localVar:300
未捕获的全局变量 globalVar:100 
--- main 访问 globalVar --- 100

合法捕获示例:

示例1:纯演示“捕获”,不返回任何闭包

func demoCapture(): Unit {
    let outerLet = 1
    var outerVar = 2

    // 立即调用闭包(不逃逸)
    { =>
        println("outerLet = ${outerLet}, outerVar = ${outerVar}")
    }()  //立即执行括号
}

main(): Int64 {
    demoCapture()
    return 0
}

说明,其中{ => ... }() 定义了一个无参数的闭包(lambda 表达式),紧跟着的一对圆括号,表示立即调用这段刚写好的代码。 它捕获了外部变量:

    outerLet(不可变的局部变量)

    outerVar(可变的局部变量)

整个表达式执行完后,闭包就运行完了,生命周期结束,没有逃逸。

示例2:返回捕获变量的闭包,但捕获的是 let,因此可逃逸

// 1. 先定义 Counter 类
class Counter {
    public var value: Int64 = 0
}

// 2. 闭包捕获 let 引用,可逃逸
func makeCounter(): () -> Int64 {
    let c = Counter()
    let closure = { =>
        c.value += 1
        return c.value
    }
    return closure
}

// 3. 主函数
main(): Int64 {
    let counter = makeCounter()
    println(counter())  // 1
    println(counter())  // 2
    return 0
}

三、要点解说

1、闭包的核心特性:脱离作用域仍可访问捕获的变量

闭包的关键能力是:即使定义变量的原始作用域消失,闭包仍能访问这些变量。这是因为闭包 “携带” 了捕获的变量。

示例:闭包脱离作用域后仍可访问变量

// 定义一个函数,返回一个闭包(嵌套函数+捕获的变量)
func getAdder(): (Int64) -> Int64 {
    let base = 100 // 局部变量,将被捕获
    
    // 嵌套函数add捕获base
    func add(num: Int64): Int64 {
        return base + num 
    }
    return add // 返回闭包(add + 捕获的base)
}

main() {
    let adder = getAdder() 
    // 此时getAdder的作用域已结束(base本应销毁),但闭包仍能访问base
    print(adder(50)) // 输出150 (即100 + 50)
}

2、变量捕获的规则

捕获变量时需遵守两个基本规则,否则编译报错:

(1).变量必须在闭包定义时可见(即闭包定义前已声明);

(2).变量必须在闭包定义时已初始化(不能捕获未初始化的变量)。

规则(1)示例  

a.如下报错

func f() {
    let x = 10
    println(x)
    let f2 = { => println(y) } // ❌ 编译报错:y未定义,无法捕获
    let y = 20 // y在闭包定义后声明
    f2()
}

main() {
    f()
}

b. 如下OK

func f() {
    let x = 10
    println(x)
    let y = 20 // y在闭包定义前声明
    let f2 = { => println(y) } // ✅编译OK
    f2()
}

main() {
    f()
}

规则(2)示例

a.如下报错

func f() {
    let x: Int64 // 声明但未初始化
    // 闭包f1定义时,x未初始化
    func f1() { print(x) } // ❌  编译报错:x未初始化,无法捕获
    x = 30 // 初始化在闭包定义后
    f1()
}

main() {
    f()
}

b. 如下OK

func f() {
    let x: Int64 // 声明
    x = 30 // 初始化    
    func f1() { print(x) } // ✅  编译OK
    f1()
}

main() {
    f()
}

3、捕获变量的修改限制

捕获变量后,能否修改取决于变量的类型和声明方式:

(1).若捕获的是let声明的值类型(如Int64):闭包内不能修改变量本身(let是不可变的);

(2).若捕获的是let声明的引用类型(如类实例):闭包内可以修改实例的可变成员(因为let仅限制变量本身不可重新赋值,不限制其成员)。

示例:

class Counter {
    var count: Int64 = 0
}

func getIncrementer(): () -> Int64 {
    let c = Counter()          //  let声明的引用类型变量
    func increment(): Int64 {
        c.count += 1           // 修改可变成员
        return c.count
    }
    return increment  // 返回闭包
}

main(): Int64 {
    let inc = getIncrementer()
    println(inc())   // 1  (即c.count变为1)
    println(inc())   // 2  (即c.count变为2)
    return 0
}

(3).若捕获的是var声明的变量(可变变量):闭包内可以修改,但会触发严格的使用限制——在仓颉中,闭包捕获可变变量不允许逃逸

为防止闭包 “逃逸”(脱离原始作用域后可能引发的不可控修改),仓颉对捕获了var变量的闭包施加严格限制:

  •  只能被调用(如g());

  •  不能作为 “一等公民” 使用:不能赋值给变量、不能作为参数传递、不能作为返回值返回、不能直接作为表达式使用。

示例

func f() {
    var x = 10 // var声明的可变变量
    
    // g捕获了var变量x
    func g() {
        x += 1 // 允许修改x
        print(x)
    }
    
    g() // 合法:仅调用
    
    //let a = g // 报错:不能赋值给变量
    //return g // 报错:不能作为返回值
}

main() {
    f()
}

附录、仓颉闭包与Python/Java闭包的异同

闭包的本质定义在所有语言中是一致的:

函数 + 其访问的外部变量环境 = 闭包

这种组合允许函数记住并访问定义时的上下文,即使在其原始作用域消失后依然有效。这一核心思想在仓颉、Python和Java中完全通用。

差异点:

差异点

仓颉

Python

Java

捕获范围

仅局部变量

所有外层变量

局部变量

可变性限制

var捕获禁止逃逸

nonlocal声明可修改

必须final或等效final

全局变量

自由访问(非捕获)

global声明修改

自由访问

简单地说,仓颉:编译期锁死风险;Python/Java:运行时留活口给开发者。

提示,如果学过其它语言的闭包,如从 Python/Java 转向仓颉的闭包,感到不习惯是很正常的 —— 这三种语言对闭包的设计思路差异有点大。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐