目录

仓颉编程语言青少年基础教程函数上

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

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

仓颉的函数融合了现代静态类型语言的诸多优点,强调安全性、表现力和高性能。有人说:仓颉把“编译时就能干掉 bug (错得少)+ 写起来代码简洁(写得少) + 跑起来像 C(跑得快)”当成硬指标,所以功能特性较多,此话不虚。不要期望“一次性学完所有特性”,需要循序渐进学习,反复突破。

函数是一段完成特定任务的独立代码片段,可以通过函数名字来标识,这个名字可以被用来调用函数。

仓颉编程语言通过关键字func来定义一个函数,func 之后依次是函数名、参数列表、可选的函数返回值类型、函数体。其中,函数名可以是任意的合法标识符,参数列表定义在一对圆括号内(多个参数间使用逗号分隔),参数列表和函数返回值类型(如果存在)之间使用冒号分隔,函数体定义在一对花括号内。func前可以用函数修饰符进行修饰。

要特别注意,与C/C++、Python等语言不同,仓颉禁止参数重新赋值——函数参数均为不可变(immutable)变量,在函数定义内不能对其赋值。

基本示例:计算和并返回

// 函数体: 
func add(a: Int, b: Int) : Int {
    return a + b  // 返回Int类型结果,与声明的返回值类型一致
}

main(): Int64 {
    // 调用函数(接收返回值)
    let sum = add(3, 5)
    println(sum)  // 输出:8
    println(add(3, 5)) // 输出:8
    return 0
}

简要说明:

参数列表((a: Int, b: Int))和函数返回值类型(Int)之间用冒号分隔( : Int);参数之间用逗号分隔。

函数体需包含return语句,返回与声明类型一致的值。

编译运行截图:

https://i-blog.csdnimg.cn/direct/0bfe52dd54504f649c2b3a7235ff099b.png

下面展开介绍仓颉编程语言的函数。

定义函数

(一)定义函数语法

仓颉中使用 func 关键字定义函数,基本结构为:

func 函数名(参数列表): 可选返回值类型 { 函数体 }。

函数名:合法标识符;

参数列表:定义在圆括号内,多个参数用逗号分隔;

返回值类型:可选,位于参数列表与函数体之间,用冒号连接;

函数体:定义在花括号内,包含函数执行的操作。

例如:
func add(a: Int64, b: Int64): Int64 {
    return a + b
}

函数修饰符

仓颉编程语言中,可以使用访问修饰符来控制对顶层函数的可见性。

【全局函数(Global Function)也即顶层函数(top-level function)就是直接写在源文件里、不包在任何 class、struct、interface 或 namespace 块内的函数。】

仓颉语言有 4 种访问修饰符:private、internal、protected、public,默认修饰符为 internal。

  •  private 表示仅当前文件内可见。不同的文件无法访问这类成员。

  •  internal 表示仅当前包及子包(包括子包的子包)内可见。同一个包内可以不导入就访问这类成员,当前包的子包(包括子包的子包)内可以通过导入来访问这类成员。

  •  protected 表示仅当前模块内可见。同一个包的文件可以不导入就访问这类成员,不同包但是在同一个模块内的其他包可以通过导入访问这些成员,不同模块的包无法访问这些成员。

  •  public 表示模块内外均可见。同一个包的文件可以不导入就访问这类成员,其他包可以通过导入访问这些成员。

【相关官方文档 】

示例:

// private修饰符(表示该函数仅在当前文件内可访问)
private func getMax(x: Int, y: Int) : Int {
    // 返回最大值
    if (x > y) {
        return x
    }
    return y
}

main(): Int64 {
    // 调用修饰符函数
    var maxNum = getMax(10, 15)
    println(maxNum)  // 输出:15
    maxNum = getMax(10, 7)
    println(maxNum)  // 输出:10
    return 0
}

(二)参数列表

func foo(x: Int64, y!: Int64 = 10) {}

注意红色标出的括号部分,函数参数分为非命名参数和命名参数,可包含 0 个或多个参数。

1). 非命名参数

定义形式:p: T(p 为参数名,T 为类型)。

调用时直接传递表达式,无需指定参数名。如

a: Int64

2). 命名参数

定义形式:p!: T = e,(参数名p后加 !,T 为类型,e 为默认值表达式)。

调用时需用 p: e 形式传递参数,且传参顺序可与定义顺序不同。如

a!: Int64 = 1

3). 参数规则:非命名参数必须在命名参数之前,并且:

  •  仅命名参数可设置默认值,非命名参数不可;

  •  非命名参数必须位于命名参数之前(命名参数后不能出现非命名参数);

     ❌ 错误示例:func add(a!: Int64, b: Int64) { … }(命名参数 a 位于非命名参数 b 前);

  •  函数参数均为不可变变量,函数体内不能对其赋值;

     ❌ 错误示例:func add(a: Int64) { a = 5 }(试图修改参数 a);

  •  参数作用域:从定义处到函数体结束,不可在函数体内重定义同名变量;

     ❌ 错误示例:func add(b: Int64) { var b = 3 }(重定义 b)。

(三)函数返回值类型

函数返回值类型可显式定义或由编译器推导,需满足返回值与类型匹配。

  1. 显式定义

在参数列表后用 : T 指定返回值类型,要求函数体中所有 return e 的 e 类型为 T 的子类型。

例如:
func fun1(a: Int64, b: Int64): Int64 { 
    return a + b // 正确:a+b 为 Int64 类型
}

func fun2(a: Int64, b: Int64): (Int64, Int64) { 
    return (a, b) //返回元组
}
如下是错误示例:返回类型不匹配
func fun2(a: Int64, b: Int64): Int64 { 
    return (a, b) // 错误:返回元组,与 Int64 不匹配
}

  1. 编译器推导

若未显式定义返回值类型,编译器会根据函数体最后一项或 return 表达式推导。

func add(a: Int64, b: Int64) { 
    return a + b // 编译器推导返回值类型为 Int64
}

注意:若推导失败(如返回值类型不统一),编译器会报错。

  1. 特殊返回类型 Unit

指定返回类型为 Unit 时,编译器会在所有可能返回的位置自动插入 return (),确保返回类型为 Unit(无实际返回值)。

func printSum(a: Int64, b: Int64): Unit { 
    println(a + b) // 无需显式 return,编译器自动处理——自动 return ()
}

(四)函数体

函数体包含变量定义、表达式、嵌套函数等,可通过 return 终止执行并返回值。

  1. return 表达式

return expr:返回表达式 expr 的值,要求 expr 类型与函数返回值类型匹配;

return:等价于 return (),仅允许在返回类型为 Unit 的函数中使用。

a) return expr 示例:
func add(a: Int64, b: Int64): Int64 { 
    return a + b 
}

b) return 示例(返回类型为 Unit):
func foo(): Unit { 
    println(“Hello”)
    return // 等价于 return ()
}

注意:return 表达式的类型为 Nothing(无实际类型)。

2. 局部变量与作用域

函数体内定义的变量为局部变量,作用域从定义处到函数体结束;

函数体内定义的变量称为 “局部变量”,其作用域从 “定义处” 到 “函数体结束”。若局部变量与外层作用域变量(如全局变量) 同名,局部变量会 “遮盖”(Shadow) 外层变量(即优先使用局部变量)。

// 全局变量:r = 0
let r = 0
 
func addAndCover(a: Int64, b: Int64): Int64 {
    // 局部变量:r = 0(与全局变量同名)
    var r = 0
    r = a + b // 使用局部变量 r
    return r
}
 
main() {
    let result = addAndCover(3, 4)
    println("局部变量 r = ${result}") // 输出:局部变量 r = 7
    println("全局变量 r = ${r}") // 输出:全局变量 r = 0(未被修改)
}

关于作用域(scope)以后还会专题介绍。

3. 函数体类型

函数体的类型由最后一项决定:

  •  最后一项为表达式 → 函数体类型为该表达式类型;

  •  最后一项为变量定义、函数声明,或函数体为空 → 函数体类型为 Unit。

示例:

a) 最后一项为表达式(Int64 类型)
func add(a: Int64, b: Int64): Int64 { 
    a + b // 等价于 return a + b
}

b) 最后一项为函数调用(print 返回 Unit)
func foo(): Unit { 
    let s = “Hi”
    print(s) // 函数体类型为 Unit
}

4. 函数禁止参数重新赋值

仓颉函数禁止参数重新赋值——函数参数均为不可变(immutable)变量,在函数定义内不能对其赋值。也就是说,仓颉函数参数与 let 声明的变量一样,只能读取,不能再次赋值。例如:

func demo(x: Int64): Int64 {
    x = x + 1   // ❌ 编译错误
    return x
}

如果需要修改,只能引入新的局部变量:

func demo(x: Int64): Int64 {
    let y = x + 1   // ✅ 正确
    return y
}

也不能在函数体内重新定义同名变量。例如:

func demo(x: Int64): Int64 {
    // x = x + 1      // ❌ 不能给不可变形参赋值
    // var x = 10     // ❌ 不能重新定义同名变量
    var y = x + 1     // ✅ 用新名字没问题
    return y
}
 
main() {
    println(demo(5))  // 输出 6
}

调用函数

调用形式:函数名(实参1, 实参2, …),实参类型需为对应参数类型的子类型。实参()可以有 0 个或多个,当实参个数为 0 时,调用方式为 函数名()。

  1. 非命名参数调用
    直接传递表达式,无需指定参数名。
    func add(a: Int64, b: Int64): Int64 { return a + b }
    let result = add(2, 3) // 直接传值,result = 5

  2. 命名参数调用
    需用 p: e 形式传递,传参顺序可与定义顺序不同。
    func add(a!: Int64, b!: Int64): Int64 { return a + b }
    let result1 = add(a: 2, b: 3) // 按定义顺序,result1 = 5
    let result2 = add(b: 3, a: 2) // 换顺序,result2 = 5

  3. 带默认值的命名参数调用
    未传参时使用默认值,传参时覆盖默认值。
    func add(a: Int64, b!: Int64 = 2): Int64 { return a + b }
    let r1 = add(1) // 未传 b,用默认值 2 → 1+2=3
    let r2 = add(1, b: 5) // 传 b=5 → 1+5=6

调用函数规则

a.非命名参数调用

必须按定义顺序传递,不能指定参数名。

b.命名参数调用

必须用 参数名: 值 形式传递,顺序可任意调整,且支持使用默认值。

c.混合参数调用(非命名 + 命名)

非命名参数必须先定义、先传递,命名参数在后,且命名参数必须指定参数名。

示例:

// 定义包含命名参数的函数(带默认值)
func calculateTotal(price: Int64, quantity!: Int64 = 1, discount!: Float64 = 0.0): Int64 {
    let total = Float64(price * quantity)      // 先转成 Float64
    let discounted = total * (1.0 - discount)    // 浮点运算
    return Int64(discounted)                     // 再转回整数
}
 
main() {
    // 调用方式1:只传非命名参数(price),命名参数用默认值
    let total1 = calculateTotal(100)  // quantity默认1,discount默认0 → 结果100
    println(total1)
 
    // 调用方式2:指定部分命名参数(可调整顺序)
    let total2 = calculateTotal(100, discount: 0.2, quantity: 2)  
    // 计算:100 * 2 * (1-0.2) = 160 → 结果160
    println(total2)
 
    // 调用方式3:显式指定所有参数名
    let total3 = calculateTotal(200, quantity: 3, discount: 0.1)  
    // 计算:200 * 3 * 0.9 = 540 → 结果540
    println(total3)
}

返回值类型的说明

在 仓颉语言(Cangjie) 的正式规范中:

必须显式写出返回类型,当返回类型为 Unit 时可省略 。

// ✅ 合法

func add(a: Int64, b: Int64): Int64 { a + b }

// ❌ 不合法- 函数体中有返回值的表达式时必须声明返回类型

func add(a: Int64, b: Int64) { a + b }

当函数没有有意义的返回值(即返回 Unit),可以写成 : Unit,也可以 直接省略 返回类型部分。

// 等价写法

func log(msg: String) { println(msg) }       // 省略 : Unit

func log(msg: String): Unit { println(msg) }  // 显式 : Unit

log(“你好”) //调用

单值返回和多值返回Tuple类型

单值返回示例:

// 返回单个 Int64
func square(n: Int64): Int64 {
    return n * n
}

main(): Unit {
    let s = square(7)   
    println("square = ${s}")  //square = 49
}

多值返回(Tuple)示例:

// 返回一个二元组 (Int64, Int64)
func divmod(a: Int64, b: Int64): (Int64, Int64) {
    return (a / b, a % b)
}

main(): Unit {
    let (q, r) = divmod(10, 3)   // q = 3, r = 1
    println("quotient = ${q}, remainder = ${r}")  //quotient = 3, remainder = 1
}

函数类型

函数是一等公民(First-class Function)

【相关官方文档 】

函数本身也有类型,称之为函数类型。

格式为:

(参数类型1, 参数类型2, …) -> 返回类型

说明,左侧 ( ) 内定义参数类型(多个参数用逗号分隔),右侧定义返回类型。

函数类型既可以根据函数定义隐式存在,也可以由程序员在代码中显式地书写出来。

  1. 隐式的函数类型 (由函数定义产生)

例子:

// 【函数定义】
// 程序员写的是具体的实现
func add(a: Int64, b: Int64): Int64 {
    return a + b
}

// 【隐式的函数类型】
// 编译器会自动识别出这个函数有一个类型:(Int64, Int64) -> Int64
// 这个类型是“依据函数定义存在的”,程序员没有显式写出 (Int64, Int64) -> Int64 这几个字。

  1. 显式的函数类型 (由程序员主动书写)

你完全可以先写出类型,再去找或定义一个符合该类型的函数(甚至用变量、lambda、函数值等)。

例子

// 1. 【显式地用于变量声明】
// 程序员主动写下了类型注解 : (Int64, Int64) -> Int64
let myMathOperator: (Int64, Int64) -> Int64 = add // 将函数add赋值给变量

// 2. 【显式地用于函数参数】
// 程序员定义了一个高阶函数,它接受一个函数作为参数
// 参数 operation 的类型被显式地定义为 (Int64, Int64) -> Int64
func calculate(operation: (Int64, Int64) -> Int64, x: Int64, y: Int64) -> Int64 {
    return operation(x, y)
}

// 3. 【显式地用于解决重载歧义】
func add(i: Int64, j: Int64) -> Int64 { i + j }
func add(i: Float64, j: Float64) -> Float64 { i + j }

// 这里直接写 add 编译器不知道选哪个,产生歧义
// let f = add // Error!

// 程序员通过【显式地书写类型】来告诉编译器需要哪个函数
let f: (Int64, Int64) -> Int64 = add // OK

补充说明

1.仓颉编程语言中函数中->符号的作用,可见: 】

2.****“函数是一等公民”和“函数类型”之间的关系

编程中的“一等公民”意味着某种实体(在这里是函数)享有和其他基础类型(如整数、字符串)同等的权利——被存进变量、能当实参、能当返回值。

在仓颉里,凡是能被存进变量、能当实参、能当返回值的,都必须先有一个类型。

没有类型,编译器就无法给变量分配空间、无法做类型检查,也就谈不上“传来传去”。

“函数类型”是“函数是一等公民”这一特性在静态类型语言中的必然要求和实现基础。

**“**函数是一等公民”是什么意思?

编程中的**“一等公民”**意味着某种实体(在这里是函数)享有和其他基础类型(如整数、字符串)同等的权利。具体来说,就是函数可以:

(1).被赋值给变量

(2).作为参数传递给另一个函数

(3).作为另一个函数的返回值被返回

如果一个语言支持这些操作,我们就说这个语言里的函数是“一等公民”。

“函数类型”又是什么?

在静态类型语言(如仓颉)中,每一个值都有其特定的类型。Int64 是整数的类型,String 是字符串的类型。编译器需要知道所有值的类型,以便在编译期间进行类型检查,确保代码的安全性。

既然函数可以作为一个值被使用(这是它作为一等公民的权利),那么它也必须有一个类型,这样编译器才能对它进行类型检查。这个类型就是函数类型

函数类型描述了函数的“形状”或“签名”,包括:

  •  它接受什么类型的参数(参数的数量和每个参数的类型)

  •  它返回什么类型的值

例如,(Int64, Int64) -> Int64 就是一个函数类型,描述了一个“接受两个 Int64 参数并返回一个 Int64 值”的函数。

仓颉是静态类型语言两者的关系表

**“一等公民”**的权利如果没有**“函数类型”**会怎样?**“函数类型”**起到的作用
赋值给变量let myFunc = add 编译器不知道 myFunc 是什么类型,无法进行后续的类型检查。let myFunc:  (Int64, Int64) -> Int64  = add 函数类型明确告知编译器:myFunc 只能存储这种特定签名的函数。之后调用 myFunc(1, 2) 时,编译器能确保参数数量和类型正确。
作为参数传递func calculate(op, a, b) 编译器完全不知道 op 是什么,应该怎么用。调用 op(a, b) 是极其危险的。func calculate(op:  (Int64, Int64) -> Int64 , a: Int64, b: Int64) 函数类型严格定义了参数 op 必须符合的格式。调用者必须传入匹配的函数,编译器确保了调用的安全性。
作为返回值func getOperator() 编译器不知道返回了什么,调用者也不知道返回的函数该怎么用。func getOperator():  (Int64, Int64) -> Int64 函数类型明确声明了返回的是一个什么样的函数。调用者拿到返回值后,可以安全地使用它。

简单地说,仓颉是静态类型语言,因为函数是一等公民(可以被当作值使用),所以必须有 “函数类型” 来规范这种使用。

示例:

// 1. 定义一个函数,“函数类型”: (Int64, Int64) -> Int64
func add(a: Int64, b: Int64): Int64 {
    a + b
}

// 2. 函数是一等公民:可以赋值给变量
let operation: (Int64, Int64) -> Int64 = add;  // ✅ 类型匹配

// 3. 函数是一等公民:可以作为参数传递
func calculate(x: Int64, y: Int64, op: (Int64, Int64) -> Int64): Int64 {
    op(x, y)  // 调用传进来的函数
}

// 4. 使用
main() {
    let result = calculate(3, 4, operation);  // 把函数作为参数传进去
    println(result); // 输出 7
}

在仓颉中,函数是一等公民,和其他值(如整数、字符串)地位平等。具体表现为:

  • 可以将函数赋值给变量

  • 可以将函数作为参数传递给其他函数(高阶函数)

  • 可以将函数作为其他函数的返回值

函数可以当值,函数名、lambda、嵌套函数都能赋值、传参、返回。

函数名本身也是表达式,它的类型为对应的函数类型。例如:

func add(a: Int64, b: Int64): Int64 { a + b }

main(): Int64 {
    let f: (Int64, Int64) -> Int64 = add   // 函数名可以赋值给变量
    println("${f(3, 4)}")              // 7
    return 0
}

函数作为参数 / 返回值,例如:

// 高阶函数:接收一个 (Int64)->Int64 的函数
func twice(g: (Int64) -> Int64, x: Int64): Int64 {
    g(g(x))
}

func inc(n: Int64): Int64 { n + 1 }

main(): Int64 {
    println("${twice(inc, 5)}")   // 7
    return 0
}

函数类型基本知识点说明

  1. 基本形式

  •  无参数、返回 Unit:() -> Unit;

  •  1 个 Int64 参数、返回 Unit:(Int64) -> Unit;

  •  2 个 Int64 参数、返回 Int64:(Int64, Int64) -> Int64。

示例

func hello(): Unit { println(“Hello”) } // 类型:() -> Unit
func add(a: Int64, b: Int64): Int64 { return a + b } // 类型:(Int64, Int64) -> Int64

  1. 类型参数名

可给函数类型的参数标记显式名称(仅用于标识,不影响类型匹配),且需统一写或统一不写,不能混合。

func show(name: String, age: Int64): Unit { 
    println("${name} is ${age}") 
}
// 变量类型指定参数名
let handler: (name: String, age: Int64) -> Unit = show 
handler(“Alice”, 18) // 调用时无需写参数名
❌ 错误示例:let h: (name: String, Int64) -> Unit(混合写参数名)。

  1. 函数类型的应用

  •  作为参数类型:函数接收另一个函数作为参数;

  •  作为返回类型:函数返回另一个函数;

  •  作为变量类型:函数赋值给变量。

(1)作为参数类型

// 接收 (Int64, Int64) -> Int64 类型的函数作为参数
func printResult(f: (Int64, Int64) -> Int64, a: Int64, b: Int64): Unit {
    println(f(a, b))
}
main(){
    // 定义一个匹配类型的函数
    func add(a: Int64, b: Int64): Int64 { return a + b }
    // 调用
    printResult(add, 2, 3) // 输出 5
}

(2)作为返回类型

func add(a: Int64, b: Int64): Int64 { return a + b }
// 返回 (Int64, Int64) -> Int64 类型的函数
func getAdd(): (Int64, Int64) -> Int64 {
    return add
}
main(){
    // 调用
    let f = getAdd()
    println(f(2, 3)) // 输出 5
}

(3)作为变量类型

func multiply(a: Int64, b: Int64): Int64 { return a * b }
// 变量类型为 (Int64, Int64) -> Int64
main() {
    let op: (Int64, Int64) -> Int64 = multiply 
    println(op(2, 3)) // 输出 6
}
  1. 重载函数的歧义

若函数被重载(同名不同参数),直接使用函数名作为表达式可能产生歧义,需显式指定类型。

// 重载 add 函数:处理 Int64 类型
func add(a: Int64, b: Int64): Int64 {
    return a + b
}

// 重载 add 函数:处理 Float64 类型
func add(a: Float64, b: Float64): Float64 {
    return a + b
}

main() {
    // ❌ 错误示例:直接将重载函数名赋值给变量,产生歧义
    // 编译器无法确定是 (Int64, Int64) -> Int64 还是 (Float64, Float64) -> Float64 类型
    // let f = add  // 此处会报错:ambiguous function 'add'

    // ✅ 正确示例1:显式指定类型为 (Int64, Int64) -> Int64,匹配处理整数的 add 函数
    let intAdd: (Int64, Int64) -> Int64 = add
    let intResult = intAdd(2, 3)  // 调用整数版本的 add,结果为 5
    println("整数相加结果:${intResult}")  // 输出:整数相加结果:5

    // ✅ 正确示例2:显式指定类型为 (Float64, Float64) -> Float64,匹配处理浮点数的 add 函数
    let floatAdd: (Float64, Float64) -> Float64 = add
    let floatResult = floatAdd(2.5, 3.5)  // 调用浮点数版本的 add,结果为 6.0
    println("浮点数相加结果:${floatResult}")  // 输出:浮点数相加结果:6.0
}

嵌套函数

定义在源文件顶层的函数被称为全局函数。

定义在函数体内的函数被称为嵌套函数。

嵌套函数是指定义在其他函数体内的函数(与之相对,定义在源文件顶层的函数称为全局函数)。其核心特性是可在定义它的外层函数内调用,也可作为返回值传递到外层函数外调用。

基本定义与使用示例:

func foo() {
    // 嵌套函数:定义在foo函数体内
    func nestAdd(a: Int64, b: Int64) {
        a + b + 3  // 函数体最后一项为表达式,返回其值
    }

    // 1. 在定义它的外层函数内调用
    println(nestAdd(1, 2))  // 输出:6(1+2+3)

    // 2. 作为返回值返回
    return nestAdd
}

main() {
    let f = foo()  // 接收返回的嵌套函数
    let x = f(1, 2)  // 在外部调用嵌套函数
    println("result: ${x}")  // 输出:result: 6
}

关键特性

嵌套函数的作用域被限制在其外层函数体内,外层函数外无法直接访问(需通过返回值传递);

可访问外层函数的局部变量(若捕获变量则构成闭包,见下文 “闭包” 部分)。