【仓颉语言入门 · 第12课】
【仓颉语言入门 · 第12课】Lambda 与高阶函数
第 11 课结尾我们尝了个鲜:
applyTwice(3, {x => x + 1})——没有名字的小函数直接当参数传。这节课把这套写法讲透:Lambda(匿名函数)的完整语法、函数类型推断的规矩、函数作参数与返回值的高阶函数、尾随 Lambda、以及map/filter/forEach/fold/reduce等把循环"说"出来的函数式工具,最后用它们重写第 5 课那类"循环里套一堆中间变量"的数据处理。本文所有代码与报错文案均在仓颉 SDK 1.1.3 下逐行实测编译运行。途中会澄清五个从别的语言抄来的高频误会:没有隐式
it、没有::函数引用、多参/多语句 Lambda 的参数必须写类型注解、forEach里的return跳不出外层函数、reduce返回的是Option而Array根本没有sum/max/find/count {}这些"想当然"的方法。
目录(系列导航)
整套路线共 7 个模块、30 课,按每周 2~3 课的节奏,大约 2~3 个月可以走完一遍:
| 模块 | 课次 | 内容 |
| — | — | — — |
| 一、环境与入门 | 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 数据处理程序
一、先感受:把"动作"当数据传
假设我们要反复对一个数字做变换。按第 11 课的写法,每种变换都得先正经定义一个命名函数:
func square(x: Int64): Int64 {
return x * x
}
func addOne(x: Int64): Int64 {
return x + 1
}
func applyTwice(x: Int64, f: (Int64) -> Int64): Int64 {
return f(f(x))
}
main(): Int64 {
println("${applyTwice(3, square)}") // 81:套两遍平方,9×9
println("${applyTwice(3, addOne)}") // 5
return 0
}
能用,但 addOne 这种只用一次的小函数,单独起名、单独占一段,啰嗦。仓颉允许你把函数体直接"端"到调用处——这就是 Lambda(匿名函数):
main(): Int64 {
println("${applyTwice(3, { x => x * x })}") // 81
println("${applyTwice(3, { x => x + 1 })}") // 5
return 0
}
{ x => x + 1 } 整体是一个值:吃一个 x,箭头 => 后是函数体。它没有名字、用完即弃,却和命名函数 square 一样能传、能调。当一段逻辑只在一个地方出现,Lambda 就是它最省事的归宿。
二、Lambda 语法全貌
2.1 无参、单参、多参
箭头左边是参数,右边是函数体:
main(): Int64 {
let f0: () -> Int64 = { => 42 } // 无参:箭头左边留空
let f1: (Int64) -> Int64 = { x => x + 1 } // 单参
let f2: (Int64, Int64) -> Int64 = { a, b => a + b } // 多参逗号分隔
println("${f0()}") // 42
println("${f1(10)}") // 11
println("${f2(3, 4)}") // 7
return 0
}
注意无参 Lambda 的箭头不能省:必须写 { => 42 },写 { 42 } 只是一个代码块、不是函数。
2.2 参数可以带类型
参数名后用冒号标类型,和普通函数一致:
let f3 = { x: Int64 => x * 2 } // 独立定义时自带类型,无需再标函数类型
println("${f3(21)}") // 42
2.3 多条语句:最后一个表达式就是返回值
函数体可以写很多行,最后一个表达式的值就是 Lambda 的返回值(无需 return),这条规则与普通函数、if、match 完全一致:
let f4: (Int64) -> Int64 = { x =>
let y = x + 1
y * 2 // 最后一行 = 返回值
}
println("${f4(10)}") // 22
2.4 类型推断的规矩:什么时候必须写注解
Lambda 的参数类型很多时候能省——靠"目标类型"反推。比如 map 知道元素是 Int64,{ x => x * 2 } 里的 x 自然就是 Int64。但有两类写法在 1.1.3 实测会推不出来,直接报错:
情况一:多个参数、又没有明确目标类型。
main(): Int64 {
let g = { a, b => a * b } // ❌
println("${g(3, 4)}")
return 0
}
error: parameters of this lambda expression must have type annotations
情况二:给 map 这类回调写多语句 Lambda、又不标参数类型(即使只有一个参数):
let cleaned = [" hi ", " ok "].map { s => // ❌ 多语句 + 参数无注解
let t = s
"[${t}]"
}
同样报 parameters of this lambda expression must have type annotations。给参数补上类型即可:
let cleaned = [" hi ", " ok "].map { s: String =>
let t = s
"[${t}]"
}
📌 稳妥记忆法:① 直接传给
map/filter等、且函数体只有一个表达式——省略注解最常见;② 用let独立定义多参 Lambda、或多语句回调遇到上面的报错——老老实实写参数类型或函数类型。推断边界在不同 API 上并不完全一致(例如第八节sort的by:比较器是多语句也能推断的),不必死记,拿不准就写类型,多写不报错。
🚫 辟谣一:仓颉没有隐式
it。 Kotlin / Swift 里单参 Lambda 可以偷懒不写参数、用it指代。仓颉不行——参数必须显式起名。写nums.filter { > 2 }这种"省略参数"的形式,编译器报expected expression or declaration, found '>'。前面例子里那个it(如{ it => it > 3 })能跑过,只是因为it被当成了你自己起的普通参数名,并非关键字。
三、高阶函数:函数作参数、作返回值
"高阶函数"就是接收函数当参数,或返回一个函数的函数。第 11 课已见过作参数的 applyTwice,这里看两个完整形态。
3.1 把动作参数化
// 把一个动作重复 n 次;action 是命名参数,带默认值
func repeat(n: Int64, action!: () -> Unit = { => println("默认动作") }): Unit {
for (_ in 0..n) {
action()
}
}
main(): Int64 {
repeat(3) // 省略:打印 3 次"默认动作"
repeat(2, action: { => println("敲代码") }) // 传入自定义动作
return 0
}
repeat 不关心动作具体是什么,谁调用谁决定——这就是"控制流与业务逻辑分离"。forEach、map 全是这个思路。
3.2 函数也能当返回值
函数的返回类型同样可以是函数类型,于是函数可以"生产"函数:
func pick(op: String): (Int64, Int64) -> Int64 {
if (op == "mul") {
return { a, b => a * b }
}
return { a, b => a + b }
}
main(): Int64 {
let f = pick("mul")
println("${f(3, 4)}") // 12
println("${pick("add")(3, 4)}") // 7:拿到返回的函数立刻调用
return 0
}
注意这里返回的 Lambda 没有引用外层变量,所以类型推断顺利。若 Lambda"记住"了外层局部变量(比如返回
{ x => x + base }),它就升级成了闭包——那是第 13 课的主题。本节只需知道:函数和数字、字符串一样,既能当参数也能当返回值。
四、尾随 Lambda:括号可以收起来
当 Lambda 是最后一个实参时,仓颉允许把它写到括号外面,括号本身可以省略:
let nums = [1, 2, 3, 4, 5]
nums.filter({ x => x % 2 == 0 }) // 常规写法
nums.filter { x => x % 2 == 0 } // 尾随 Lambda:等价,括号省了
nums.fold(0, { acc, x => acc + x }) // 初始值在括号里,Lambda 也可移到括号外
nums.fold(0) { acc, x => acc + x } // 等价:fold(初始值) { ... }
两行写法完全等价。链式调用时尾随形式读起来更像"声明"而不是"调用":
let r = nums.filter { x => x > 1 }
.map { x => x * 10 }
.fold(0) { acc, x => acc + x }
println("${r}") // 140:2,3,4,5 → 20,30,40,50 → 140
规则只有一条:挪到括号外的必须是最后一个参数。Lambda 前面若还有普通实参(如
fold的初始值0),它们留在括号里。
五、集合的函数式工具箱
Array<T> 与 ArrayList<T> 自带一批"喂 Lambda"的方法,下面全部在 1.1.3 实测,两者行为一致。
5.1 map:逐个变换
把每个元素按你的规则换成新值,元素类型也可以变(Int64 → String 都行):
let nums = [1, 2, 3]
println("${nums.map { x => x * 2 }}") // [2, 4, 6]
println("${nums.map { x => "No.${x}" }}") // [No.1, No.2, No.3]
5.2 filter:按条件筛选
Lambda 返回 Bool,留下 true 的元素:
println("${nums.filter { x => x % 2 == 0 }}") // [2]
5.3 forEach:逐个消费(只重副作用)
不产出新集合,纯粹"对每个元素做点事"(打印、计数、写入),返回 Unit:
nums.forEach { x => print("${x} ") } // 1 2 3
5.4 fold:带着累加器归约
fold(初始值) { 累加器, 当前元素 => 新累加器 }——求和、拼接、求积的万能写法:
println("${nums.fold(0) { acc, x => acc + x }}") // 6
println("${nums.fold(1) { acc, x => acc * x }}") // 6(1*2*3)
第一遍 acc 是你给的初始值,之后每遍用上一遍的结果,最后返回累加器。空集合也安全:直接原样返回初始值。
5.5 reduce:没有初始值的归约(返回 Option)
reduce 不接受初始值,用第一个元素当起点;于是空集合无值可归——它用 Option 表达这种可能为空的结果(第 9 课):
println("${nums.reduce { a, b => a * b }}") // Some(6)
let empty: Array<Int64> = []
println("${empty.reduce { a, b => a + b }}") // None
println("${empty.fold(100) { a, b => a + b }}") // 100
打印出来是 Some(6) / None。取真实值用第 9 课的 match 或 ??:
let p = nums.reduce { a, b => a + b }
println("${p ?? 0}") // 6:None 时兜底 0
📌 fold 与 reduce 怎么选:集合可能为空、或希望有个明确初始值(比如金额从
0.0起),用fold;确定非空、且首元素就是天然初值,用reduce并处理Option。
5.6 判定、切片与摊平
let a = [10, 20, 30, 40, 50]
println("${a.all { x => x % 2 == 0 }}") // true:全部满足
println("${a.any { x => x > 40 }}") // true:存在满足
println("${a.take(2)}") // [10, 20]:取前 2 个
println("${a.slice(1, 4)}") // [20, 30, 40, 50]:下标 1 到 4
println("${a.indexOf(30)}") // Some(2)
println("${a.indexOf(99)}") // None
let nested = [[1, 2], [3], [4, 5, 6]]
println("${nested.flatMap { xs => xs }}") // [1, 2, 3, 4, 5, 6]:摊平一层
⚠️
slice(start, end)的两个端点都包含(闭区间),slice(1, 4)拿到的是下标 1、2、3、4 共 4 个元素,和某些语言"含头不含尾"的习惯相反,别少取一个。
5.7 这些"想当然"的方法在 1.1.3 并不存在
实测下面这些名字都不是 Array 的成员(编译器报 'xxx' is not a member of struct 'Array<...>'):
| 你可能想写 | 1.1.3 的替代 |
|---|---|
nums.sum() / nums.max() / nums.min() | nums.fold(0) { a, b => a + b } 等自己归约 |
nums.count { x => ... }(谓词计数) | nums.filter { ... }.size |
nums.find { ... } / nums.first { ... } | let m = nums.filter { ... }; if (m.size > 0) { m.first } |
nums.sorted() / nums.takeLast() / nums.reversed() | 排序用全局 sort(下一节);其余自行组合 |
first / last 是属性不是方法(第 7 课),不能在后面挂 Lambda。这也是为什么本节工具链以 map / filter / fold 为主——它们足以组合出上述全部需求。
六、自定义排序:sort 与 Ordering
第 7 课学过 sort(nums) 做自然升序、sort(nums, descending: true) 降序(import std.sort.*,原地排序)。想按自己的规则排(比如按字符串长度、按元组里的金额),用命名参数 by: 传一个比较器 Lambda:
import std.sort.*
main(): Int64 {
var words = ["bbb", "a", "cc"]
sort(words, by: { a: String, b: String =>
if (a.size > b.size) {
Ordering.GT
} else if (a.size < b.size) {
Ordering.LT
} else {
Ordering.EQ
}
})
println("${words}") // [a, cc, bbb]:按长度升序
return 0
}
比较器收到两个元素,返回三值枚举 Ordering:
| 返回值 | 含义 | 排序后 |
|---|---|---|
Ordering.LT | a 小于 b | a 排在前 |
Ordering.EQ | 两者相等 | 稳定排序时保持原序 |
Ordering.GT | a 大于 b | a 排在后 |
Ordering 是枚举(第 18 课系统讲),本节照抄三个值即可。好消息是所有可比较类型自带 compare 方法直接产出 Ordering,比较器可以极简:
var more = [5, 1, 4, 2]
more.sortBy(comparator: { a, b => a.compare(b) }) // [1, 2, 4, 5]
⚠️ 实测点:点方法
sortBy(comparator: ...)在 1.1.3 可用但已标记弃用(编译有 deprecated 警告),官方推荐全局函数sort(数据, by: { ... })(stable:/descending:两个命名参数都有默认值false,可省略)。课程统一用推荐写法。排序是原地修改,想保留原顺序先
clone()(第 7 课)。
七、forEach 里的 return:跳过本次,不是跳出整个循环
这是最容易踩的语义坑。看实测:
func test(): Unit {
let nums = [1, 2, 3]
nums.forEach({ x =>
if (x == 2) {
return // 结束的是"这一次 Lambda 调用"
}
println("x=${x}")
})
println("after forEach")
}
输出:
x=1
x=3
after forEach
x == 2 时的 return 只提前结束了当次回调(等价于普通循环里的 continue),循环继续处理 3,forEach 之后的语句也照常执行。它不会从外层 test 函数返回。想"找到就整体停止",老老实实用第 5 课的 for + break/return:
for (x in nums) {
if (x == 2) {
return // 这才是从 test 整体返回
}
}
顺带两个实测限制:① Lambda 里可以读外层的
let变量(如nums.map { x => x * factor }),但捕获var可变变量会受"必须直接调用"限制(报lambda capturing mutable variables needs to be called directly)——需要在 Lambda 里改外部计数器时先退回普通 for 循环;闭包的完整规则第 13 课讲。② 仓颉没有::函数引用语法,传命名函数直接写名字即可:nums.forEach(printInt);写nums.forEach(::println)会报expected expression after '(', found '::'。
八、CIDE 实操:奶茶店订单分析
8.1 编写程序
新建工程(或单文件),输入下面的程序。订单用第 11 课的元组表示 (品名, 杯数, 单价),综合运用 map / filter / forEach / fold / reduce、尾随 Lambda、自定义排序和命名函数复用。
import std.sort.*
// 单笔金额 = 杯数 × 单价
func amount(order: (String, Int64, Float64)): Float64 {
return Float64(order[1]) * order[2]
}
main(): Int64 {
let orders = [
("珍珠奶茶", 2, 12.5),
("杨枝甘露", 1, 18.0),
("柠檬水", 3, 6.0),
("生椰拿铁", 2, 19.0)
]
// ① 每笔金额:元组 -> 数字
let amounts = orders.map { o => amount(o) }
println("各笔金额: ${amounts}")
// ② 筛出大额订单并逐条打印
let big = orders.filter { o => amount(o) >= 20.0 }
println("大额订单 ${big.size} 笔:")
big.forEach { o => println(" ${o[0]}:${amount(o)}") }
// ③ 总营业额:金额可能为空表,fold 从 0.0 起最稳妥
let total = orders.fold(0.0) { acc, o => acc + amount(o) }
println("总营业额: ${total}")
// ④ reduce 的 Option 语义:空集合返回 None,不崩溃
let empty: Array<Float64> = []
println("空表 reduce: ${empty.reduce { a, b => a + b }}")
println("空表 fold: ${empty.fold(0.0) { acc, x => acc + x }}")
// ⑤ 按金额降序:先 clone 保留原表,比较器返回 Ordering
var byAmount = orders.clone()
sort(byAmount, by: { a, b =>
let da = amount(a)
let db = amount(b)
if (da > db) { Ordering.LT } else if (da < db) { Ordering.GT } else { Ordering.EQ }
})
println("金额降序:")
byAmount.forEach { o => println(" ${o[0]} ${amount(o)}") }
// ⑥ 总杯数与平均每笔
let cups = orders.map { o => o[1] }.fold(0) { a, b => a + b }
println("总杯数: ${cups},平均每笔: ${total / Float64(orders.size)}")
return 0
}
8.2 运行与验证
在 CIDE 中点运行(或 cjpm run),应得到与下面逐字一致的输出(浮点数默认保留 6 位小数):
各笔金额: [25.000000, 18.000000, 18.000000, 38.000000]
大额订单 2 笔:
珍珠奶茶:25.000000
生椰拿铁:38.000000
总营业额: 99.000000
空表 reduce: None
空表 fold: 0.000000
金额降序:
生椰拿铁 38.000000
珍珠奶茶 25.000000
杨枝甘露 18.000000
柠檬水 18.000000
总杯数: 8,平均每笔: 24.750000
拿它和"命令式写法"对比着读:同样的逻辑用第 5 课的循环写,需要 var total = 0.0、var cups = 0、循环里嵌套 if、再单独写排序;函数式写法没有任何可变中间变量,每一步"输入集合 → 输出集合",读代码就是在读需求本身。
可以自己动手改:
- 把 ② 的条件改成"杯数不少于 2",观察
filter结果; - 把 ③ 的
fold改成reduce,再对空表调用一次,体会Some/None与初始值的差别; - ⑤ 的降序改成按杯数升序(比较
order[1],可借助Int64.compare); - 故意写
orders.sum()、big.first { ... }、nums.filter { > 1 },亲眼看编译器报的not a member/expected expression错误。
8.3 用调试器观察累加器
- 在 ③
fold的 Lambda 行(acc + amount(o))打断点,Shift + F9 启动调试; - 反复 F8 单步,在 Variables 中观察
acc的变化:0.0 → 25.0 → 43.0 → 61.0 → 99.0,这就是 fold"累加器"名字的由来; - 在 ② 的
filterLambda 断点处观察每个o,返回true/false决定该笔订单去留; - 进到 ⑤ 的比较器,观察
Ordering.LT/GT如何指挥元素交换位置。
调试结束点停止按钮。
九、常见问题 FAQ
Q1:Lambda 和普通函数到底什么区别?什么时候该用?
Lambda 是没有名字的函数字面量,适合只在一处使用的短逻辑(尤其作为 map / filter 等的参数);需要在多处复用、递归调用或函数体较长时,正经定义命名函数更清晰。两者类型与调用规则完全相同,Lambda 也能先赋给变量再传递。
Q2:为什么 let g = { a, b => a * b } 报 parameters must have type annotations?可省略类型的边界在哪?
编译器需要"目标类型"才能反推 Lambda 的参数类型。直接传给 map / filter 等、且为单表达式时通常可省;但多参 Lambda 用 let 独立定义、以及给 map 写多语句回调时,1.1.3 会推断不出并报 parameters of this lambda expression must have type annotations,写注解({ a: Int64, b: Int64 => a * b })或声明函数类型(let g: (Int64, Int64) -> Int64 = ...)即可。注意推断边界随 API 不同略有差异(sort 的 by: 比较器多语句也能推断),拿不准就写类型。
Q3:仓颉 Lambda 能用隐式 it 吗?参数能省略吗?
不能。参数必须显式命名,写 filter { > 2 } 报 expected expression or declaration, found '>'。Kotlin 风格的 it 只是一个合法的自定义参数名,不是关键字。
Q4:尾随 Lambda 是什么?什么情况下能把括号省了?
Lambda 是最后一个实参时可写在调用括号外:a.filter { ... } 等价于 a.filter({ ... });前面还有普通实参时把它们留在括号内,如 a.fold(0) { acc, x => acc + x }。
Q5:fold 和 reduce 有什么区别?空集合会怎样?
fold(初始值) 每一遍都有累加器,空集合直接返回初始值,适合"可能为空 / 需要明确起点"的场景;reduce 无初始值、以首元素为起点,结果是 Option——非空返回 Some(结果),空集合返回 None,取值用 match 或 ??。
Q6:为什么 nums.sum() / nums.max() / nums.find {} / nums.count {} 都报 not a member?
SDK 1.1.3 的 Array 没有这些方法。求和/极值用 fold 自己归约;谓词计数用 filter { ... }.size;找第一个匹配用 filter { ... } 后判断 size 再取 first(first 是属性不是方法)。
Q7:在 forEach 的 Lambda 里写 return,为什么循环没有停?
Lambda 里的无值 return 只结束当次回调,效果等同 continue,不会从外层函数返回,也不会中断遍历。要整体跳出,改用 for 循环配合 break 或在外层函数中 return。
Q8:怎么把命名函数传给高阶函数?有 :: 引用符吗?
仓颉没有 ::(写 forEach(::println) 报 expected expression after '(', found '::')。直接写函数名即可:nums.forEach(printInt),函数名本身就是一个函数类型的值。
Q9:自定义排序的比较器为什么要返回 Ordering.LT/GT/EQ,不能返回 Bool 吗?
sort(数据, by: 比较器) 的比较器类型是 (T, T) -> Ordering,三值枚举才能同时表达"小于 / 相等 / 大于"(相等关系到稳定排序是否保序)。想省事可直接返回 a.compare(b)(可比较类型自带,直接产出 Ordering)。枚举的完整讲解在第 18 课。
Q10:println("${orders.filter { ... }}") 报 should implement ToString?
如果数组元素是元组(比如第 11 课的 (String, Int64)),不能整体插值打印——SDK 1.1.3 的元组没有实现 ToString 接口(含元组的 Option 同理)。改成 forEach 逐字段打印:orders.forEach { o => println("${o[0]} ${o[1]}") },或先 map { o => "${o[0]}:${o[1]}" } 变成字符串数组再打印。普通数值/字符串数组没有这个限制。
十、课后练习
- 给定
[1, 2, 3, 4, 5, 6, 7, 8],用一条链式调用完成"只保留偶数 → 各自平方 → 求总和",打印结果(预期 120)。再把它改写成等价的单个fold。 - 写高阶函数
func repeat(n: Int64, action!: (Int64) -> Unit): Unit,对0..n的每个整数调用一次action(i);分别用 Lambda 打印"第 i 次"和打印i * i。 - 给定一句英文
"the cangjie language is fun",用split(' ')切成数组,配合map/filter(过滤掉空串)/fold统计:单词个数与单词平均长度(总字符数 ÷ 个数,用Float64(...)转换后相除)。 - 用元组数组表示成绩
[(String, Int64)](姓名、分数),完成:筛出及格(≥60)的人、用fold求及格总分、用sort(..., by: ...)按分数降序排列后逐条打印。比较器可借助a[1].compare(b[1]),注意降序要交换LT/GT。 - 打开练习 1 的工程 Shift+F9 调试,在
fold/map的 Lambda 内打断点,单步观察累加器与每次变换的中间值;再把某个 Lambda 改成多语句并故意省略参数类型注解,记录must have type annotations报错后补全类型。
下节预告
这节课的 Lambda 都很"安分"——进来什么算什么,不依赖外部。但你可能已经注意到:nums.map { x => x * factor } 里的 factor 不是参数,也不是 Lambda 内部定义的,它是从外层"抓"进来的。第 13 课 闭包、作用域与函数类型 就专门研究这件事:Lambda 如何捕获外层变量、let 与 var 捕获的差别、嵌套函数的作用域边界,以及函数类型在变量、参数、返回值之间的完整规则。我们下节课见!
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入(例如新版本补充了
sum/find/getOrElse等方法、或放开了 Lambda 参数推断限制),以你本地版本为准,欢迎评论区交流。
💬 遇到问题?扫码联系作者
跟着课程练习时,如果在 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)