仓颉编程语言的闭包
一个函数或 lambda 从定义它的静态作用域中捕获了变量,函数或 lambda 和捕获的变量一起被称为一个闭包。仓颉语言的闭包是 “函数 /lambda + 捕获的外部变量” 的组合,其目的是允许函数或 lambda 访问定义时的外部变量,即使脱离原始作用域仍能使用这些变量。仓颉的闭包设计以 “安全性” 为核心,通过严格限制 “捕获可变变量(var变量)的闭包” 的逃逸行为,避免跨作用域的意外修
仓颉编程语言的闭包
一、仓颉闭包的定义
一个函数或 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 转向仓颉的闭包,感到不习惯是很正常的 —— 这三种语言对闭包的设计思路差异有点大。
更多推荐

所有评论(0)