##仓颉##
在编程语言中,闭包(Closure) 是一个强大的概念,它由函数(或 Lambda 表达式) 及其从定义它的静态作用域中捕获的变量共同组成。这种捕获机制使得闭包即使在其定义的作用域之外被调用,也能正常访问和使用这些变量,仿佛它们仍在原作用域中一般。
变量捕获的本质
以下情形构成变量捕获:
- 外部局部变量访问:在函数或 Lambda 内部访问了在其外部定义的局部变量。
- 默认参数的外部依赖:函数的参数默认值中使用了外部定义的局部变量。
- 非成员函数访问成员:在类或结构体内部定义的非成员函数或 Lambda 访问了该实例的成员变量或
this
指针。
非捕获情形
以下访问不视为变量捕获:
- 内部局部变量:访问函数或 Lambda 自身内部定义的变量。
- 形参访问:访问函数自身的形参。
- 全局/静态访问:访问全局变量或类的静态成员变量。
- 成员函数访问成员:在成员函数或属性中访问实例成员变量(本质是通过传入的
this
参数访问)。
捕获规则与编译约束
变量捕获发生在闭包定义时,而非调用时,因此有严格的编译期规则:
- 可见性规则:被捕获的变量必须在闭包定义的位置可见。若变量声明在闭包之后,则无法捕获,导致编译错误。
func f() { let f2 = { => println(y) } // 错误:y 尚未定义,不可见 let y = 88 f2() }
- 初始化规则:被捕获的变量在闭包定义时必须已完成初始化。使用未初始化的变量会导致编译错误。
func f() { let x: Int64 func f1() { println(x) } // 错误:x 在闭包定义时未初始化 x = 99 f1() }
捕获的威力:跨作用域访问
闭包的核心价值在于它能“记住”并携带其诞生环境:
func returnAddNum() -> (Int64) -> Int64 {
let num: Int64 = 10 // 外部局部变量
func add(a: Int64) -> Int64 {
return a + num // 捕获 num
}
return add // 闭包(add + num)被返回
}
func main() {
let f = returnAddNum() // f 是闭包,携带 num=10
print(f(10)) // 在 main 作用域调用,仍能访问 num,输出 20
}
捕获引用类型:状态修改
当捕获引用类型变量时,闭包可通过该引用修改其状态:
class C {
public var num: Int64 = 0
}
func returnIncrementer() -> () -> Void {
let c = C() // 引用类型实例
func incrementer() {
c.num += 1 // 捕获 c,修改其成员
}
return incrementer // 闭包(incrementer + c 的引用)被返回
}
func main() {
let increment = returnIncrementer()
increment() // c.num 变为 1
increment() // c.num 变为 2
}
可变变量捕获的限制:闭包逃逸
为安全起见,捕获了 var
声明(可变)局部变量的闭包受到严格限制:
- 不能作为一等公民:不能赋值给变量、不能作为参数传递、不能作为返回值、不能直接作为表达式使用。
- 仅限调用:只能在其定义的作用域内被直接调用。
func restrictedExample() {
var x = 1
let y = 2
func g() { print(x) } // 捕获 var x
// let b = g // 错误:不能赋值(闭包逃逸)
// return g // 错误:不能返回
g() // 允许:直接调用
}
捕获的传染性
限制具有传染性:若函数 f
调用了捕获外部 var
变量的函数 g
,且 g
捕获的 var
不在 f
内部定义,则 f
自身也被视为捕获了该 var
,同样受到一等公民使用的限制。
func h() {
var x = 1 // 外部 var
func g() { _ = x } // g 捕获外部 var x
func f() {
g() // f 调用了捕获外部 var 的 g
}
// return f // 错误:f 被传染限制,不能返回
}
例外:若 g
捕获的 var
在 f
内部定义,则 f
不受传染。
func validExample() -> () -> Void {
func f() {
var x = 1 // var 在 f 内部定义
func g() { _ = x } // g 捕获 f 内部的 var
g()
}
return f // 允许:f 未捕获外部 var
}
全局与静态:捕获的例外
访问全局变量或类的静态成员变量不构成变量捕获。因此,即使修改 var
声明的全局或静态变量,相关函数仍可作为一等公民自由使用。
class C {
static var a: Int32 = 0 // 静态成员变量 (var)
}
var globalV1 = 0 // 全局变量 (var)
func countGlobal() {
globalV1 += 1
C.a = 99
}
func user() {
let counter = countGlobal // 允许:未捕获局部 var
counter()
}
结语
理解闭包与变量捕获机制是掌握现代编程语言的关键。它赋予函数“记忆”其诞生环境的能力,实现了强大的功能如状态保持、回调函数和延迟计算。牢记捕获的触发条件、编译期规则以及对可变变量捕获的限制,有助于编写出既强大又安全的闭包代码。
##仓颉##