仓颉编程语言青少年基础教程:函数(下)

接着上一讲,继续介绍仓颉编程语言的函数知识。

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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐