【仓颉语言入门 · 第13课】
【仓颉语言入门 · 第13课】闭包、作用域与函数类型
第 12 课我们用 Lambda 写了一堆"一次性动作",但有个细节没展开:
nums.map { x => x * factor }里的factor不是参数,也不是 Lambda 内部定义的——它是从外层"抓"进来的。这节课把这件事讲透:变量的作用域边界、闭包如何捕获外层变量、let与var捕获的根本差别、可变状态的正确封装方式,以及函数类型在变量、参数、返回值、容器中的完整规则。本文所有代码与报错文案均在仓颉 SDK 1.1.3 下逐行实测编译运行。途中会澄清三个高频误区:仓颉的
var不能被存起来的 Lambda 捕获(只能"直接调用")、for 循环变量是按迭代独立绑定的(没有 JS 那种经典闭包陷阱)、Lambda 里的return只从 Lambda 本身返回(非局部返回不支持带值)。
目录(系列导航)
整套路线共 7 个模块、30 课:
| 模块 | 课次 | 内容 |
| — | — | — — |
| 一、环境与入门 | 01~05 | 环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环 |
| 二、常用类型与数据组织 | 06~10 | 字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理 |
| 三、函数与函数式 | 11~14 | 函数、Lambda 与高阶函数、闭包、迭代器与惰性序列 |
| 四、面向对象与类型系统 | 15~20 | struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展 |
| 五、工程化与标准库 | 21~25 | cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试 |
| 六、并发编程 | 26~28 | 线程、Channel 通道与同步原语、并发实战 |
| 七、项目实战 | 29~30 | 命令行小工具、GeoJSON 数据处理实战 |
- 环境搭建与第一个仓颉程序
- 变量与常量:let / var 与基本数据类型
- 运算符与标准输入输出
- 分支结构:if 与 match 表达式
- 循环结构:while / for / Range
- 字符串详解与字符串插值
- 数组 Array 与区间 Range
- 集合框架:ArrayList、HashMap、HashSet
- 可空类型
?与 Option - 错误处理:异常机制与 Result
- 函数定义、参数与返回值
- Lambda 与高阶函数
- 闭包、作用域与函数类型(本文)
- 迭代器 Iterator 与 Sequence
- 结构体 struct 与类 class
- 构造函数、属性与方法
- 接口 interface 与实现
- 枚举 enum、代数数据类型与 match 模式匹配
- 泛型编程
- 扩展、类型别名与可见性控制
- cjpm 包管理与多文件项目组织
- 文件与目录 IO
- JSON 处理(结合 stdx 扩展库)
- 网络编程入门
- 单元测试
- 并发基础:线程的创建与等待
- Channel 通道与同步原语
- 并发实战:多线程任务处理
- 实战一:带文件持久化的命令行小工具
- 实战二:GeoJSON 数据处理程序
一、作用域:变量住在哪里
"作用域"就是一个变量能被访问到的代码范围。仓颉采用词法作用域(静态作用域):变量能在哪用,写代码时就定死了,跟运行时谁调用无关。
1.1 三层作用域
let globalVar = 100 // 全局作用域:整个文件都能用
func demo(n: Int64): Int64 {
let localVar = n + 1 // 函数作用域:只在 demo 内部
if (localVar > 0) {
let blockVar = 42 // 块级作用域:只在这个 if 块里
println("${globalVar} ${localVar} ${blockVar}")
}
// println(blockVar) // ❌ 编译错误:找不到 blockVar
return localVar
}
全局作用域的变量任何函数都能读;函数作用域的变量只在函数体内;块级作用域(if / for / while 的 {})变量只在块内。
1.2 变量遮蔽(Shadowing)
内层可以声明与外层同名的变量,此时内层变量"遮住"外层的:
main(): Int64 {
let x = 10
println("outer x=${x}") // 10
if (x > 0) {
let x = 20 // 遮蔽外层 x
println("inner x=${x}") // 20
}
println("after x=${x}") // 10:外层 x 没变
return 0
}
📌 遮蔽是"换了个新变量",不是"修改旧变量"。外层的
x在内层被遮蔽期间完全不受影响,出了内层块还是原来的值。想修改外层变量,老老实实重新赋值(前提是它是var)。
🚫 仓颉没有独立的"裸代码块"语法。 写
{ let y = 100 }会被编译器当成 Lambda({ => ... }的简写),不是作用域块。想创建临时作用域,用if (条件) { ... }或for/while的块。do { ... }只用于 do-while 循环。
二、闭包:函数 + 它记住的环境
闭包 = 函数 + 该函数定义时所处的词法环境。通俗说,函数会"记住"它被创建时能看到的那些变量,即使创建它的外层函数已经执行完毕、那些变量按理说该"消失"了。
2.1 捕获 let(不可变变量)
这是最常见、最安全的闭包形式:
func makeAdder(base: Int64): (Int64) -> Int64 {
return { x => x + base } // base 是参数(let 语义),被 Lambda 捕获
}
main(): Int64 {
let add5 = makeAdder(5)
println("${add5(10)}") // 15
println("${add5(20)}") // 25
return 0
}
makeAdder(5) 执行完后,参数 base 按理说该销毁了,但返回的 Lambda 还"记着"它,所以每次调用都能加上 5。这就是闭包的核心能力。
2.2 捕获 var(可变变量):有限制
let 是不可变的,捕获了也改不了,所以闭包拿着很安全。但 var 是可变的,闭包如果能随意修改外层 var,就会引入难以追踪的副作用。仓颉对此的态度是:存起来的 Lambda(赋值给变量、作为返回值、传给别的函数)一律不能捕获 var,只有**“当场直接调用”**的 Lambda 可以。
func makeCounter(): () -> Int64 {
var n = 0
return { => n += 1; n } // ❌ n 是 var,Lambda 被返回了
}
error: lambda capturing mutable variables needs to be called directly
note: 'n' is mutable
"直接调用"就是 Lambda 写完立刻加 () 执行,不存、不传:
main(): Int64 {
var n = 0
{ => n += 1 }() // ✅ 当场调用,允许
println("${n}") // 1
return 0
}
⚠️
nums.forEach { x => sum += x }也不行——虽然forEach会调用这个 Lambda,但它属于"传参",编译器判定为非直接调用,照样报lambda capturing mutable variables needs to be called directly。要在遍历中累加,用fold(第 12 课)或退回到for循环。
三、可变状态怎么办:容器模式
闭包需要"记住一个会变的值"怎么办?核心思路是:不要捕获 var 本身,而是捕获一个指向可变容器的 let。容器本身不可变(let 引用),但容器里装的内容可变。
3.1 数组当容器(最简单)
func makeCounter(): () -> Int64 {
let cell = [0] // cell 是 let,但 cell[0] 可改
return { =>
cell[0] = cell[0] + 1
cell[0]
}
}
main(): Int64 {
let c = makeCounter()
println("${c()} ${c()} ${c()}") // 1 2 3
return 0
}
cell 是 let,指向同一个数组;Lambda 通过 cell[0] 修改数组元素,绕过了"不能捕获 var"的限制。适合单个数值。
3.2 class 当容器(推荐,语义清晰)
class CounterCell {
var value: Int64 = 0
public func get(): Int64 { return value }
public func set(v: Int64) { value = v }
}
func makeCounter(): () -> Int64 {
let cell = CounterCell()
return { =>
cell.set(cell.get() + 1)
cell.get()
}
}
class 的实例是引用类型,cell 是 let 但 cell.value 可变。这是工程里最清晰的写法。
📌
var字段的修改方法必须能修改实例状态——在 class 里普通func即可修改var字段(class 默认是引用语义)。struct是值类型,修改var字段的方法需要特殊处理,优先用 class 做可变容器。
3.3 HashMap 当容器(记忆化 / 缓存)
import std.collection.*
func makeMemoized(): (Int64) -> Int64 {
let cache = HashMap<Int64, Int64>()
return { n =>
if (cache.contains(n)) {
return cache.get(n) ?? 0
}
let result = n * n
cache.add(n, result) // 注意:HashMap 用 add,不是 put/set
result
}
}
cache 是 let,但 HashMap 内部可增删。适合键值对形式的可变状态,做记忆化缓存(memoization)的经典套路。
3.4 多个闭包共享状态
用同一个容器,可以让多个闭包读写同一份状态:
class AccountCell {
var balance: Int64 = 0
public func get(): Int64 { return balance }
public func deposit(v: Int64) { balance += v }
public func withdraw(v: Int64): Bool {
if (balance >= v) { balance -= v; return true }
return false
}
}
func createAccount(initial: Int64): ((Int64) -> Unit, (Int64) -> Bool, () -> Int64) {
let cell = AccountCell()
cell.deposit(initial)
return (
{ v => cell.deposit(v) },
{ v => cell.withdraw(v) },
{ => cell.get() }
)
}
三个闭包(存款、取款、查余额)共享同一个 AccountCell,实现了"对象"的效果,却没有用 class 的方法,全靠闭包 + 容器。
四、函数类型完全指南
第 11、12 课零散见过函数类型,这里系统梳理。函数类型的写法是:
(参数1类型, 参数2类型, ...) -> 返回类型
无参函数:() -> 返回类型
4.1 作为变量类型
func add(a: Int64, b: Int64): Int64 { return a + b }
func sub(a: Int64, b: Int64): Int64 { return a - b }
main(): Int64 {
let op: (Int64, Int64) -> Int64 = add
println("${op(3, 4)}") // 7
var current = add
current = sub // 同类型的函数可以互相赋值
println("${current(3, 4)}") // -1
return 0
}
4.2 作为参数 / 返回值
// 参数是函数
func apply(op: (Int64, Int64) -> Int64, a: Int64, b: Int64): Int64 {
return op(a, b)
}
// 返回值是函数
func pick(op: String): (Int64, Int64) -> Int64 {
if (op == "mul") { return { a, b => a * b } }
return { a, b => a + b }
}
4.3 作为数组 / 集合元素
函数类型跟 Int64、String 一样,能放进容器:
let ops: Array<(Int64, Int64) -> Int64> = [
{ a, b => a + b },
{ a, b => a - b },
{ a, b => a * b }
]
for (op in ops) {
print("${op(8, 3)} ") // 11 5 24
}
4.4 类型别名:type
函数类型写长了啰嗦,用 type 起别名:
type OpType = (Int64, Int64) -> Int64
func apply(op: OpType, a: Int64, b: Int64): Int64 {
return op(a, b)
}
🚫 仓颉用
type,不是typealias。写typealias OpType = ...报expected declaration, found 'typealias'。
4.5 函数类型的 Option 包装
函数也可以"可空",用 Option<函数类型> 包装:
let handler: Option<(Int64) -> Int64> = None
println("${handler.isNone()}") // true
let h2: Option<(Int64) -> Int64> = Some({ x => x * 2 })
let fn = h2 ?? { x => 0 }
println("${fn(5)}") // 10
注意可空函数类型的写法是
Option<(Int64) -> Int64>,不要写成((Int64) -> Int64)?——后者会报语法错误。
五、循环变量捕获:仓颉没有经典陷阱
JavaScript 里有个经典坑:for (var i = 0; i < 3; i++) 中创建的闭包会共享同一个 i,最终都是 3。仓颉没有这个问题——for 循环的变量每次迭代都是独立绑定:
import std.collection.*
main(): Int64 {
let factories = ArrayList<() -> Int64>()
for (i in 1..=3) {
factories.add({ => i * 10 })
}
for (f in factories) {
print("${f()} ") // 10 20 30:各自记住当时的 i
}
return 0
}
输出 10 20 30,三个闭包各记各的。这是因为仓颉的 i 是 let 语义,每轮循环一个新绑定。
六、嵌套函数
函数可以定义在另一个函数内部,内层函数能直接访问外层的局部变量(这本身就是闭包):
func outer(n: Int64): Int64 {
func inner(): Int64 {
return n * 2 // 直接用外层的 n
}
return inner()
}
main(): Int64 {
println("${outer(5)}") // 10
return 0
}
嵌套函数只在外层函数体内可见,外层执行完它就消失。适合把"只在一个地方用的小逻辑"就近定义,不用提升到全局。
七、return 语义总结
第 12 课讲了 forEach 里的 return,这里把所有情况归拢:
| 场景 | return 的效果 |
|---|---|
| 普通函数里 | 从该函数返回 |
for / while 循环里 | 从外层函数返回(带值) |
Lambda 里、无值 return | 只结束当次 Lambda 调用(类似 continue) |
Lambda 里、带值 return | ❌ 编译报错 |
Lambda 带值 return 的报错:
func findTarget(): Int64 {
let nums = [1, 2, 3]
nums.forEach { x =>
if (x == 2) {
return x // ❌
}
}
return 0
}
error: the return type of this function cannot be calculated from the function body
note: The types 'Int64' and 'Unit' do not have the smallest common supertype
📌 想在遍历中"找到就返回",用
for循环配合return;Lambda 适合"对每个元素做点事",不适合"提前终止整个流程"。这是仓颉的设计取舍:Lambda 的return永远是局部的,不会从外层函数"非局部返回"。
八、CIDE 实操:可配置计算器工厂
8.1 编写程序
综合运用:闭包捕获不可变配置、闭包通过 class 容器共享可变状态、函数类型作为参数和返回值、循环变量捕获、函数类型数组。
import std.collection.*
// 可变状态容器:操作计数
class OpCounter {
var count: Int64 = 0
public func get(): Int64 { return count }
public func inc() { count += 1 }
}
// 工厂:包装两个运算,自动计数
func makeCalculator(
name: String,
add: (Int64, Int64) -> Int64,
mul: (Int64, Int64) -> Int64
): (String, (Int64, Int64) -> Int64, (Int64, Int64) -> Int64, OpCounter) {
let counter = OpCounter()
let safeAdd: (Int64, Int64) -> Int64 = { a, b =>
counter.inc()
add(a, b)
}
let safeMul: (Int64, Int64) -> Int64 = { a, b =>
counter.inc()
mul(a, b)
}
return (name, safeAdd, safeMul, counter)
}
func makeCounter(): () -> Int64 {
let cell = [0]
return { => cell[0] = cell[0] + 1; cell[0] }
}
main(): Int64 {
// ① 配置一个普通计算器
let (name, add, mul, counter) = makeCalculator(
"普通",
{ a, b => a + b },
{ a, b => a * b }
)
println("=== ${name} 计算器 ===")
println("3 + 4 = ${add(3, 4)}")
println("5 * 6 = ${mul(5, 6)}")
println("操作次数:${counter.get()}")
// ② 数组容器的计数器
let c = makeCounter()
println("数组计数器:${c()} ${c()} ${c()}")
// ③ 循环变量捕获(每次迭代独立绑定)
let factories = ArrayList<() -> Int64>()
for (i in 1..=3) {
factories.add({ => i * 10 })
}
print("循环捕获:")
for (f in factories) {
print("${f()} ")
}
println()
// ④ 函数类型数组
let ops: Array<(Int64, Int64) -> Int64> = [
{ a, b => a + b },
{ a, b => a - b },
{ a, b => a * b }
]
print("3 种运算 (8,3):")
for (op in ops) {
print("${op(8, 3)} ")
}
println()
return 0
}
8.2 运行与验证
=== 普通 计算器 ===
3 + 4 = 7
5 * 6 = 30
操作次数:2
数组计数器:1 2 3
循环捕获:10 20 30
3 种运算 (8,3):11 5 24
关键点解读:
makeCalculator返回四个值(元组):名称、包装后的加法、包装后的乘法、共享的计数器。两个包装闭包都捕获同一个counter,所以调用add或mul都会让计数 +1。makeCounter用数组[0]当容器,cell[0]可改,cell本身是let。- 循环捕获输出
10 20 30,证明每次迭代的i是独立绑定。 - 函数数组
ops存放三个不同的 Lambda,遍历调用得到11 5 24。
可以自己动手改:
- 给
makeCalculator再传入一个sub运算,包装后返回; - 把数组容器换成
CounterCellclass,体会两种写法的差异; - 在 ④ 的数组里加一个
{ a, b => a / b },注意整数除法。
8.3 用调试器观察闭包状态
- 在
safeAdd的counter.inc()行打断点,Shift + F9 启动调试; - 调用
add(3, 4)时断住,观察 Variables 面板里的counter——它是从外层makeCalculator捕获的,此时count从 0 变成 1; - 再调用
mul(5, 6),同一个counter的count从 1 变成 2,证明两个闭包共享状态; - 在
makeCounter的cell[0] = cell[0] + 1行打断点,观察cell数组的变化。
调试结束点停止按钮。
九、常见问题 FAQ
Q1:闭包和普通函数到底什么区别?
普通函数只依赖传入的参数;闭包除了参数,还"记住"了定义时外层环境的变量。实现上,闭包 = 函数代码 + 一份捕获的变量引用。
Q2:为什么捕获 var 会报错?捕获 let 就没事?
let 不可变,闭包拿着只读引用,不会引发副作用,编译器放心。var 可变,如果允许存起来的闭包随意修改,就可能出现"函数 A 改了函数 B 的局部变量"这种难以追踪的情况。仓颉只允许"直接调用"的 Lambda 捕获 var(当场执行完就销毁,不会留下隐患)。
Q3:想让闭包持有可变状态,有哪些写法?
三种容器模式:① 单元素数组 let cell = [0],改 cell[0];② class 实例,let cell = MyClass(),改 cell.varField;③ 集合类 HashMap / ArrayList,改内部元素。核心都是:捕获一个 let 引用,通过引用修改其内部状态。
Q4:仓颉有非局部返回(non-local return)吗?
没有。Lambda 里的 return 只从 Lambda 本身返回,永远不会跳出外层函数。带值 return 还会直接编译报错。想从遍历中提前返回,改用 for 循环。
Q5:函数类型可以做 struct 的字段吗?
可以,但 let 字段必须有默认值或在构造函数中初始化。如果字段是函数类型且不想给默认值,建议用 class 或直接用元组返回多个闭包(见第八节的 createAccount 模式)。
Q6:函数类型怎么起别名?
用 type 别名 = 函数类型,例如 type Op = (Int64, Int64) -> Int64。不要写 typealias(那是别的语言的关键字)。
Q7:闭包会内存泄漏吗?
闭包持有的变量只要闭包还活着就不会被回收。如果一个长生命周期的闭包捕获了大对象,该对象就无法释放。实际开发中注意不要让全局闭包捕获不必要的大对象即可,日常使用无需担心。
Q8:多个闭包能共享同一个容器吗?
能。只要多个闭包捕获的是同一个 let 引用(同一个 class 实例 / 同一个数组 / 同一个 HashMap),它们就共享同一份状态。这是用闭包实现"对象"和"封装"的基础。
Q9:for 循环里创建闭包,会有变量共享问题吗?
不会。仓颉的 for 循环变量是 let 语义,每次迭代独立绑定,闭包各自记住当时的值。没有 JavaScript var 那种经典陷阱。
Q10:闭包里能调用递归吗?
Lambda 不能直接递归(它没有名字,无法引用自己)。需要递归的逻辑,定义命名函数。命名函数的递归与闭包无关,正常写即可。
十、课后练习
- 用数组容器写一个
makeAccumulator():返回一个(Int64) -> Int64闭包,每次调用把参数累加到内部状态并返回累加值。例如acc(5)返回 5,再acc(3)返回 8。 - 用
class容器重写第 1 题,体会两种容器的代码风格差异。 - 写一个
makeLogger(prefix: String): (String) -> Unit,返回的闭包打印[prefix] 消息。验证makeLogger("INFO")("hello")输出[INFO] hello。 - 写
makeMultiplier(factor: Int64): (Int64) -> Int64,然后用Array存三个不同 factor 的乘法器,遍历对同一个数调用并打印结果。 - 综合:用
HashMap<String, Int64>做容器,写一个makeVoteCounter(): (String) -> Unit,每次调用给对应候选人 +1 票;再写一个getVotes(): HashMap<String, Int64>返回当前票数。在 main 中投几票后打印结果。提示:两个闭包共享同一个 HashMap。
下节预告
这三节课(11~13)把函数从"命名定义"到"匿名 Lambda"再到"闭包捕获"讲完了。但有个问题一直没解决:想"逐个产出"一系列值(比如斐波那契数列、大文件的逐行读取),难道要一次性全算出来塞进数组?第 14 课 迭代器 Iterator 与 Sequence 就来解决这个问题——用 next() 按需产出、惰性求值,配合闭包实现无限序列。我们下节课见!
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入(例如新版本放宽了
var捕获限制、补充了非局部返回等),以你本地版本为准,欢迎评论区交流。
💬 遇到问题?扫码联系作者
跟着课程练习时,如果在 SDK 安装、环境变量配置、编译报错或调试上卡住,欢迎扫码加作者企业微信直接咨询(请备注"仓颉课程"):

离线环境下图片可能加载不出来,也可以在 CIDE 菜单 Help ▸ 联系作者 / Contact 中查看同一张二维码(应用内置兜底图,无需联网)。
📥 工具下载
本系列全程使用的仓颉 IDE —— CIDE(免费开源、社区版):
- GitCode 仓库 / 安装包下载:https://gitcode.com/wp_upala/cide
- 打开页面后进入 发行版(Releases),两种包任选其一:
- 安装版:下载
CIDE-<版本>-x64-Setup.exe,双击安装,适合日常长期使用; - 免安装版(Portable):下载
CIDE-<版本>-x64-Portable.zip,解压到任意目录即用,不写注册表、不留安装痕迹,拷到 U 盘也能在别的电脑直接运行(包内附《使用说明.txt》)。适合先试用、或在受限电脑上学习本系列课程。
- 安装版:下载
- 仓颉 SDK 请前往仓颉编程语言官网下载:https://cangjie-lang.cn
更多推荐




所有评论(0)