【仓颉语言入门 · 第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 返回的是 OptionArray 根本没有 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 数据处理实战 |

  1. 环境搭建与第一个仓颉程序
  2. 变量与常量:let / var 与基本数据类型
  3. 运算符与标准输入输出
  4. 分支结构:if 与 match 表达式
  5. 循环结构:while / for / Range
  6. 字符串详解与字符串插值
  7. 数组 Array 与区间 Range
  8. 集合框架:ArrayList、HashMap、HashSet
  9. 可空类型 ? 与 Option
  10. 错误处理:异常机制与 Result
  11. 函数定义、参数与返回值
  12. Lambda 与高阶函数(本文)
  13. 闭包、作用域与函数类型
  14. 迭代器 Iterator 与 Sequence
  15. 结构体 struct 与类 class
  16. 构造函数、属性与方法
  17. 接口 interface 与实现
  18. 枚举 enum、代数数据类型与 match 模式匹配
  19. 泛型编程
  20. 扩展、类型别名与可见性控制
  21. cjpm 包管理与多文件项目组织
  22. 文件与目录 IO
  23. JSON 处理(结合 stdx 扩展库)
  24. 网络编程入门
  25. 单元测试
  26. 并发基础:线程的创建与等待
  27. Channel 通道与同步原语
  28. 并发实战:多线程任务处理
  29. 实战一:带文件持久化的命令行小工具
  30. 实战二: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 上并不完全一致(例如第八节 sortby: 比较器是多语句也能推断的),不必死记,拿不准就写类型,多写不报错

🚫 辟谣一:仓颉没有隐式 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 不关心动作具体是什么,谁调用谁决定——这就是"控制流与业务逻辑分离"。forEachmap 全是这个思路。

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:逐个变换

把每个元素按你的规则换成新值,元素类型也可以变Int64String 都行):

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.LTa 小于 ba 排在
Ordering.EQ两者相等稳定排序时保持原序
Ordering.GTa 大于 ba 排在

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.0var 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 用调试器观察累加器

  1. 在 ③ fold 的 Lambda 行(acc + amount(o))打断点,Shift + F9 启动调试;
  2. 反复 F8 单步,在 Variables 中观察 acc 的变化:0.0 → 25.0 → 43.0 → 61.0 → 99.0,这就是 fold"累加器"名字的由来;
  3. 在 ② 的 filter Lambda 断点处观察每个 o,返回 true / false 决定该笔订单去留;
  4. 进到 ⑤ 的比较器,观察 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 不同略有差异(sortby: 比较器多语句也能推断),拿不准就写类型。

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:foldreduce 有什么区别?空集合会怎样?
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 再取 firstfirst 是属性不是方法)。

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. 给定 [1, 2, 3, 4, 5, 6, 7, 8],用一条链式调用完成"只保留偶数 → 各自平方 → 求总和",打印结果(预期 120)。再把它改写成等价的单个 fold
  2. 写高阶函数 func repeat(n: Int64, action!: (Int64) -> Unit): Unit,对 0..n 的每个整数调用一次 action(i);分别用 Lambda 打印"第 i 次"和打印 i * i
  3. 给定一句英文 "the cangjie language is fun",用 split(' ') 切成数组,配合 map / filter(过滤掉空串)/ fold 统计:单词个数与单词平均长度(总字符数 ÷ 个数,用 Float64(...) 转换后相除)。
  4. 用元组数组表示成绩 [(String, Int64)](姓名、分数),完成:筛出及格(≥60)的人、用 fold 求及格总分、用 sort(..., by: ...) 按分数降序排列后逐条打印。比较器可借助 a[1].compare(b[1]),注意降序要交换 LT / GT
  5. 打开练习 1 的工程 Shift+F9 调试,在 fold / map 的 Lambda 内打断点,单步观察累加器与每次变换的中间值;再把某个 Lambda 改成多语句并故意省略参数类型注解,记录 must have type annotations 报错后补全类型。

下节预告

这节课的 Lambda 都很"安分"——进来什么算什么,不依赖外部。但你可能已经注意到:nums.map { x => x * factor } 里的 factor 不是参数,也不是 Lambda 内部定义的,它是从外层"抓"进来的。第 13 课 闭包、作用域与函数类型 就专门研究这件事:Lambda 如何捕获外层变量、letvar 捕获的差别、嵌套函数的作用域边界,以及函数类型在变量、参数、返回值之间的完整规则。我们下节课见!


系列说明:本系列基于 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
Logo

一站式 AI 云服务平台

更多推荐