仓颉编程语言青少年基础教程:函数(下)
仓颉编程语言青少年基础教程:函数(下),介绍了仓颉编程语言中Lambda表达式、闭包、函数重载等高级特性。
仓颉编程语言青少年基础教程:函数(下)
接着上一讲,继续介绍仓颉编程语言的函数知识。
Lambda 表达式
Lambda 表达式是仓颉语言中的匿名函数(无函数名),核心作用是快速定义简短逻辑,简化代码并提升灵活性。其概念源于数学中的 λ 演算,现已被多种编程语言采用。
Lambda 表达式是一种匿名函数,语法简洁,常用于临时定义简单逻辑。
基础示例源码:
// 1. 有参数:计算两个 Int64 类型的和
let add: (Int64, Int64) -> Int64 = { a: Int64, b: Int64 => a + b }
// 2. 无参数:打印文本
var printHello: () -> Unit = { =>
println("Hello, Cangjie!")
}
// 程序入口
main(): Int64 {
println("add(2, 3) = ${add(2, 3)}") // 输出:add(2, 3) = 5
printHello() // 输出:Hello, Cangjie!
return 0
}
运行截图:

定义语法
基本形式:{ 参数列表 => 函数体 }
• 参数列表:可包含 0 个或多个参数,格式为p1: T1, p2: T2, ...(参数类型可省略,由上下文推断);
• =>:用于分隔参数列表与函数体,不可省略(除非作为尾随 Lambda);
• 函数体:一组表达式或声明——可以是一个单一的表达式(其计算结果即为 Lambda 的返回值),也可以是由花括号 {} 包围的多个语句和声明 。返回类型由上下文推断(不可显式声明)。
下面详细示例解读之:
(1)带参数的 Lambda
main() {
// 显式指定参数类型的Lambda
let f1 = { a: Int64, b: Int64 => a + b }
// 正确:调用Lambda并打印结果
println("f1 result: ${f1(1, 2)}") // 输出:f1 result: 3
// 类型推断的Lambda: // 编译器推断a和b都是Int64
var sum1: (Int64, Int64) -> Int64 = { a, b => a + b }
// 正确:调用Lambda并打印结果
println("sum1 result: ${sum1(3, 4)}") // 输出:sum1 result: 7
func calculate(op: (Int64, Int64) -> Int64, x: Int64, y: Int64) {
println(op(x, y))
}
// 编译器根据calculate函数的参数类型推断Lambda类型
calculate({ a, b => a * b }, 5, 6) // 输出30
}
编译运行输出:
f1 result: 3
sum1 result: 7
30
(2)无参数的 Lambda示例:
main() {
var display = { => // 无参数,必须保留=>
println("Hello")
println("World")
}
display() //调用无参数Lambda,只需加()
}
编译运行输出:
Hello
World
(3)Lambda 表达式中不支持声明返回类型,其返回类型总是从上下文中推断出来,若无法推断则报错。
示例:
/* ------------------------------
一、能推断:上下文足够,编译通过
------------------------------*/
// 1. 变量类型已确定
let f1: (Int64)->Int64 = { x => x * 2 } // OK,返回 Int64
// 2. 作为实参,形参类型已确定
func twice(g: (Int64)->Float64): Float64 { g(21) * 2.0 }
let r2 = twice({ it => Float64(it) + 0.5 }) // OK,返回 Float64
// 3. 立即调用:先给 Lambda 本身标注类型
let r3 = ({ a: Int64, b: Int64 => a - b })(10, 3)
/* ------------------------------
二、不能推断:上下文缺失,编译报错
------------------------------*/
// ❶ 反注释下一行 → 报错:无法推断返回类型
// let bad = { x => x + 1 }
// ❷ 反注释下一行 → 报错:重载歧义,返回类型不明
// func overload(g: (Int64)->Int64) { println("int") }
// func overload(g: (Int64)->Float64) { println("float") }
// overload({ x => x * 2 }) // 两个重载都匹配,编译器无法选择
main(): Int64 {
println("f1(5) = ${f1(5)}") // 10
println("twice = ${r2}") // 43.000000
println("10-3 = ${r3}") // 7
return 0
}
编译运行输出:
f1(5) = 10
twice = 43.000000
10-3 = 7
(4)尾随 Lambda(可省略=>)示例:
【尾随 lambda后面介绍(可见函数调用语法糖的有关部分)。】
当 Lambda 作为函数的最后一个参数时,可省略=>并写在函数调用的大括号外,示例:
main() {
// 修改f2函数:执行传入的Lambda
func f2(lam: () -> Unit) {
lam() // 实际调用传入的Lambda
}
// 使用尾随Lambda语法调用f2
let f2Res = f2 {
println("World") // World
}
// 注意:f2没有返回值,所以f2Res是Unit类型
println("f2Res的值: ${f2Res}") // f2Res的值: ()
}
编译运行输出:
World
f2Res的值: ()
Lambda 表达式调用方式
• 立即调用:定义后直接传参调用;
• 赋值给变量后调用:通过变量名调用。
示例:
main() {
// 1. 立即调用
let r1 = { a: Int64, b: Int64 => a + b }(1, 2) // r1 = 3
let r2 = { => 123 }() // r2 = 123
println("r1 = ${r1}")
println("r2 = ${r2}")
// 2. 先赋值给变量,再调用
var g = { x: Int64 => println("x = ${x}") }
g(2) // 输出:x = 2
}
编译运行输出:
r1 = 3
r2 = 123
x = 2
Lambda易错点:
1) Lambda 体有多条语句时忘记换行或分号
main() {
// ❌ 易错
// let f = { a: Int64 => println("hi") a + 1 } // 解析歧义
// ✅ 正确
let f = { a: Int64 => print("hi "); a + 1 }
println("${f(12)}") // 输出: hi 13
}
也可以写为换行方式,更易读,适合复杂逻辑:
main() {
// ❌ 易错
// let f = { a: Int64 => println("hi") a + 1 } // 解析歧义
// ✅ 正确
let f = { a: Int64 =>
print("hi ");
a + 1
}
println("${f(12)}") // 输出: hi 13
}
2) 捕获未初始化变量
func b() {
let x: Int64
x = 10 // 初始化 若注释掉该行,启用 ★ 行 将出错
let g = { =>
println("尝试使用x: ${x}") // 编译错误
}
// x = 10 // 初始化太晚 ★
g()
}
main() {
b()
}
闭包(Closure)
一个函数或 lambda 从定义它的静态作用域中捕获了变量,函数或 lambda 和捕获的变量一起被称为一个闭包,这样即使脱离了闭包定义所在的作用域,闭包也能正常运行。
仓颉语言中的“闭包” = 函数 / λ 表达式 + 其静态作用域中被捕获的变量。即使离开定义作用域,闭包仍能继续访问这些变量。
与仓颉语言的闭包相关知识:
1.作用域(Scope) - 最重要的基石
• 仓颉采用词法作用域(静态作用域):变量可见性在写代码时就确定,而非运行时。
• 支持嵌套作用域:内层函数能直接读取外层函数的局部变量。
需要理解掌握 “变量在哪里可见”
例如:
func outer() {
let msg = "hello"
func inner() { println(msg) } // 合法:inner 捕获 msg
inner()
}
main(): Int64 {
outer()
return 0
}
2.生命周期(Lifetime)
• 普通局部变量随函数返回而消亡;
• 被闭包捕获的变量会自动迁到堆,因此能活得比外层函数更久。
闭包的特殊之处就在于:即使外部函数的栈帧销毁,其局部变量仍能被内部函数 “记住”
例如:
//* 返回闭包,outer 的栈帧已销毁,但 msg 仍活着 */
func makeGreeter(): () -> Unit {
let msg = "I am still alive!"
return { => println(msg) } // msg 被捕获并迁到堆
}
main(): Int64 {
makeGreeter()() // 正常打印msg, 第一个 () 拿闭包,第二个 () 立即执行
let g = makeGreeter()
g() // 正常打印msg,证明 msg 生命周期被延长
return 0
}
3. 嵌套函数(Nested Functions)与 Lambda 表达式 - 闭包的产生环境
• 嵌套函数:在一个函数内部定义的函数。
• Lambda表达式/ 匿名函数:一种简洁的定义匿名函数的方式。
例如:
/* 嵌套函数形式 */
func nestedVersion(): (Int64) -> Int64 {
let base = 100
func add(a: Int64) { // 嵌套函数
return a + base
}
return add
}
/* Lambda 形式 */
func lambdaVersion(): (Int64) -> Int64 {
let base = 100
return { a => a + base } // 匿名函数,一样捕获 base
}
main(): Int64 {
let f1 = nestedVersion()
let f2 = lambdaVersion()
println("nested: ${f1(5)}") // nested: 105
println("lambda: ${f2(5)}") // nested: 105
return 0
}
4. 在仓颉里,当你把闭包赋值给变量、或者传给函数形参时,只要编译器无法唯一推断出它的签名,就需要用函数类型显式标注;如果上下文信息足够(例如直接传给已知形参类型的函数),则可以省略类型,让编译器自动推。
示例说明:
a. 推得出来 → 不写类型
/* 形参类型已确定,编译器直接推断 */
func applyTwice(x: Int64, f: (Int64)->Int64): Int64 {
f(f(x))
}
main(): Int64 {
// 闭包签名一目了然,无需再写
let r = applyTwice(5, { it => it * 2 }) // 5→10→20
println(r) // 20
return 0
}
b. 推不出来 → 必须写类型
main(): Int64 {
// 没有上下文,编译器无法推断,不写就报错
// let f = { a, b => a + b } // ❌ 无法推断
/* 写上类型 ✅ */
let f: (Int64, Int64) -> Int64 = { a, b => a + b }
println(f(3, 4)) // 7
return 0
}
5.捕获点:编译期完成;变量在捕获点必须已初始化且可见,否则报错 。
let 变量:捕获后只读;闭包可作为一等公民(赋值、传参、返回)。
var 变量:
– 允许捕获并修改其值;
– 但闭包不能逃逸:不能赋值、不能当实参/返回值,只能原地调用
示例说明:
a.捕获 let(可逃逸)
func makeAdder() : (Int64) -> Int64 {
let base = 10 // 不可变
func adder(x: Int64) : Int64 { // 捕获 base
x + base
}
return adder // 允许逃逸
}
main() {
let f = makeAdder()
println(f(7)) // 17
}
b.捕获 var(禁止逃逸)
func demoVar() {
var counter = 0
func inc() { counter += 1 } // 捕获可变变量
inc() // OK:原地调用
// let g = inc // ❌ 编译错误:不能逃逸
println(counter) // 1
}
main() {
demoVar()
}
下面是几个简单而完整的仓颉闭包示例:
闭包示例1:
// 这个函数返回一个闭包
// 该闭包捕获了外部函数的参数 `name`
func makeGreeter(name: String): () -> String {
// 这个内部函数捕获了外部的 `name`
func greet(): String {
return "你好, ${name}!" // 这里访问了外部变量 `name`
}
return greet // 返回这个内部函数,它形成了一个闭包
}
main() {
// 创建两个独立的闭包,它们“记住”了各自创建时的不同名字
let greeterForAlice = makeGreeter("Alice")
let greeterForBob = makeGreeter("Bob")
// 调用闭包。即使 makeGreeter 函数已经执行完毕,
// 闭包仍然能访问到当时捕获的 `name`。
println(greeterForAlice()) // 输出: 你好, Alice!
println(greeterForAlice()) // 再次输出: 你好, Alice!
println(greeterForBob()) // 输出: 你好, Bob!
}
编译运行输出:
你好, Alice!
你好, Alice!
你好, Bob!
闭包示例2:
// 1. 用 class 把可变状态搬到堆上
class Counter {
var n: Int64 = 0
func next(): Int64 {
n += 1
return n
}
}
// 2. 返回闭包,lambda 只捕获不可变的 Counter 引用
func makeCounter(): () -> Int64 {
let c = Counter() // c 是 let 常量,符合规则
return { => c.next() } // lambda 形成闭包
}
// 3. 使用示例
main() {
let c1 = makeCounter()
println(c1()) // 1
println(c1()) // 2
let c2 = makeCounter()
println(c2()) // 1 (独立状态)
}
仓颉编程语言的闭包介绍 https://blog.csdn.net/cnds123/article/details/150481494
闭包官方文档https://cangjie-lang.cn/docs?url=%2F1.0.0%2Fuser_manual%2Fsource_zh_cn%2Ffunction%2Fclosure.html
函数调用语法糖
语法糖是 “简化代码的便捷写法”,不改变功能但让代码更易读。
1.尾随 Lambda:让函数调用像 “语言内置语法”
当函数最后一个参数是函数类型,且传入的是 lambda 表达式(匿名函数)时,可以把 lambda 移到函数调用的括号外面,甚至省略括号(当只有一个lambda 实参时)。
示例
// 定义函数:第一个参数是Bool,第二个是函数类型(无参返回Int64)
func myIf(condition: Bool, action: () -> Int64): Int64 {
if (condition) {
return action() // 条件为true时执行action
} else {
return 0
}
}
main() {
// 普通调用:lambda放在括号内
let result1 = myIf(true, { => 100 })
// 尾随lambda:lambda移到括号外(更像if语法)
let result2 = myIf(false) {
200 // 条件为false,不执行,返回0
}
// 当函数只有一个函数类型参数时,可省略括号
func singleFunc(fn: (Int64) -> Int64): Int64 {
return fn(3)
}
let result3 = singleFunc { num => num * 2 } // 等价于singleFunc({ num => num * 2 })
println(result1) // 输出:100
println(result2) // 输出:0
println(result3) // 输出:6
}
编译运行输出:
100
0
6
特殊情况:只有一个 lambda 参数时,连 () 都可以省略!例如:
// 只接收一个函数参数
func runTask(task: (Int64) -> Int64): Int64 {
task(5)
}
main() {
// 使用尾随 lambda,并省略 ()
let result = runTask { x =>
x * x // 计算 5*5 = 25
}
println("平方结果: ${result}") // 输出: 平方结果: 25
}
2.Flow(流)表达式
流操作符包括两种:表示数据流向的中缀操作符 |> (称为 pipeline)和表示函数组合的中缀操作符 ~> (称为 composition)。
流操作符有两个:|>(数据管道)和~>(函数组合),用于简化 “多步处理” 的代码。
(1)Pipeline(|>):数据流向管道
e1 |> e2 等价于 “先算 e1,再把结果传给 e2 作为参数”,适合依次处理数据。其中 e2 是函数类型的表达式,e1 的类型是 e2 的参数类型的子类型。
(2)Composition(~>):函数组合成新函数
f ~> g 等价于 “先执行 f,再用 f 的结果执行 g”,即{ x => g(f(x)) },其中 f,g 均为只有一个参数的函数类型的表达式。
流操作符示例:
func inc(x: Int64): Int64 { x + 1 }
func double(x: Int64): Int64 { x * 2 }
main() {
// pipeline:数据从左往右流
let a = 5 |> inc |> double // 5→6→12
println(a) // 12
// composition:把两根水管先“拧”成一根
let incThenDouble = inc ~> double // 先 inc 再 double
let b = incThenDouble(5) // 同样是 12
println(b)
}
编译运行截图:

3. 变长参数:传多个值代替数组
当函数最后一个非命名参数是 Array 类型时,可以直接传多个值(不用显式写数组)。
示例
// 计算任意数量整数的和
func sum(numbers: Array<Int64>): Int64 {
var total: Int64 = 0
for(num in numbers) {
total += num
}
total
}
main() {
// 使用变长参数语法
println(sum()) // 输出: 0
println(sum(1)) // 输出: 1
println(sum(1, 2)) // 输出: 3
println(sum(1, 2, 3)) // 输出: 6
println(sum(1, 2, 3, 4)) // 输出: 10
// 传统方式(需要创建数组)
println(sum([1, 2, 3])) // 输出: 6
}
函数重载
“重载” 指同一作用域内,函数名相同但参数不同(个数或类型),编译器会根据调用时的参数自动匹配正确的函数。重载规则:
• 参数必须不同(个数或类型);
• 静态成员函数和实例成员函数不能重载(同名会报错);
• 调用时编译器优先选 “最匹配” 的函数(如子类参数优先于父类)。
示例:
// 重载1:两个Int64相加
func add(a: Int64, b: Int64): Int64 {
return a + b
}
// 重载2:两个Float64相加
func add(a: Float64, b: Float64): Float64 {
return a + b
}
// 重载3:一个Int64,一个默认值(参数个数不同)
func add(a: Int64): Int64 {
return a + 10 // 默认加10
}
main() {
println(add(2, 3)) // 匹配重载1,输出:5
println(add(2.5, 3.5)) // 匹配重载2,输出:6.000000
println(add(5)) // 匹配重载3,输出:15
}
编译运行输出:
5
6.000000
15
操作符重载
如果需要在某个类型上重载某个操作符,可以通过为类型定义一个函数名为此操作符的函数的方式实现,这样,在该类型的实例使用该操作符时,就会自动调用此操作符函数。定义操作符函数时需要在 func 关键字前面添加 operator 修饰符。
通过重载操作符(如+、-、[]),让自定义类型(如类、结构体)像内置类型一样使用操作符。
示例:
class Point {
var x: Int64 = 0
var y: Int64 = 0
public init(a: Int64, b: Int64) { x = a; y = b }
// 一元负号
public operator func -(): Point { Point(-x, -y) }
// 二元加
public operator func +(p: Point): Point { Point(x + p.x, y + p.y) }
// 索引取值
public operator func [](i: Int64): Int64 { if (i == 0) {x} else {y}}
// 函数调用操作符
public operator func ()(): Unit { println("Point(${x}, ${y})") }
}
main() {
let p1 = Point(3, 4)
let p2 = -p1 // 一元负号
let p3 = p1 + p2 // 二元加
println(p3[0]) // 索引:0 号元素 0,故输出:0
p3() // 函数调用操作符,输出:Point(0, 0)
}
编译运行输出:
0
Point(0, 0)
const 函数和常量求值
const 函数是一类特殊的函数,这些函数具备了可以在编译时求值的能力。在 const 上下文中调用这种函数时,这些函数会在编译时执行计算,提高程序性能。
特点:
1.必须带 const 修饰符;函数体里所有表达式都必须是 const 表达式。const init 函数内的表达式不要求都是 const 表达式。
2.内部只能声明 let / const 局部变量,禁止使用 var。
3. const 实例成员函数可以访问当前类型的实例成员变量(由于 const 函数性质,这些成员必须是 let/const 类型)
4.const 变量 / 常量的初始化器必须是 const 表达式(含 const 函数调用)。
5.若类/结构体要定义 const init,则:
• 类内不能出现 var 声明的实例成员;
• 父类必须也有 const init;
• const init 里只允许对实例成员做赋值初始化,禁止其它赋值语句。
• const init 必须调用父类的 const init(可显式或隐式调用无参版本)。
示例1:
// 全局 const 变量
const PI = 3.14159
// 结构体示例
struct Point {
let x: Float64 // 必须是 let/const
let y: Float64
// const 构造器
const Point(x: Float64, y: Float64) {
this.x = x // 只允许对实例成员赋值
this.y = y
}
// const 实例成员函数
const func distanceTo(other: Point): Float64 {
let dx = this.x - other.x // 可访问实例成员
let dy = this.y - other.y
(dx*dx + dy*dy)**0.5
}
}
// 类示例
class Circle {
let radius: Float64 // 必须是 let/const
// const 构造器
const init(radius: Float64) {
this.radius = radius // 只允许对实例成员赋值
}
// const 实例成员函数
const func area(): Float64 {
PI * radius * radius // 可访问 const 全局变量和实例成员
}
}
main() {
// 编译时计算
const p1 = Point(3.0, 4.0)
const p2 = Point(0.0, 0.0)
const dist = p1.distanceTo(p2) // 编译时计算为 5.0
const c = Circle(2.0)
const area = c.area() // 编译时计算
println("Distance: ${dist}") // 输出 5.000000
println("Area: ${area}") // 输出 12.566360
}
编译运行输出:
Distance: 5.000000
Area: 12.566360
示例2:
// 定义一个简单的二维点
struct Point {
let x: Float64
let y: Float64
// const 构造函数
const init(x: Float64, y: Float64) {
this.x = x
this.y = y
}
// const 成员函数:计算到原点的距离
const func distanceToOrigin(): Float64 {
(x*x + y*y) ** 0.5
}
}
// const 函数:计算两点之间的距离
const func distance(a: Point, b: Point): Float64 {
let dx = a.x - b.x
let dy = a.y - b.y
(dx*dx + dy*dy) ** 0.5
}
main() {
// 这些计算在编译时完成
const p1 = Point(3.0, 0.0)
const p2 = Point(0.0, 4.0)
const d1 = p1.distanceToOrigin() // 编译时计算
const d2 = distance(p1, p2) // 编译时计算
println("Distance to origin: ${d1}") // 输出: 3.000000
println("Distance between points: ${d2}") // 输出: 5.000000
// 运行时计算示例
let runtimePoint = Point(5.0, 12.0)
let runtimeDistance = runtimePoint.distanceToOrigin() // 运行时计算
println("Runtime distance: ${runtimeDistance}") // 输出: 13.000000
}
编译运行输出:
Distance to origin: 3.000000
Distance between points: 5.000000
Runtime distance: 13.000000
更多推荐

所有评论(0)