华为仓颉语言的函数初步

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

基础示例:定义一个加法函数

// 显式声明返回值类型为 Int64
func add(a: Int64, b: Int64): Int64 {
    return a + b // 返回 a 和 b 的和
}

// 省略返回值类型(编译器推导为 Int64)
func addAuto(a: Int64, b: Int64) {
    a + b // 函数体最后一项为表达式,等价于 return a + b
}

// 调用函数(后续会详细讲调用方式)
main() {
    let result1 = add(10, 20)
    let result2 = addAuto(30, 40)
    println("add(10,20) = ${result1}") // 输出:add(10,20) = 30
    println("addAuto(30,40) = ${result2}") // 输出:addAuto(30,40) = 70
}

要特别注意,与C/C++、Python等语言不同,仓颉禁止参数重新赋值——函数参数均为不可变(immutable)变量,在函数定义内不能对其赋值。也就是说,仓颉函数参数与 let 声明的变量一样,只能读取,不能再次赋值。例如:
func demo(x: Int64): Int64 {
    x = x + 1   // ❌ 编译错误
    return x
}

如果需要修改,只能引入新的局部变量:
func demo(x: Int64): Int64 {
    let y = x + 1   // ✅ 正确
    return y
}

这说明,仓颉语言语言设计决策强化对不可变性和函数式编程范式的偏好。

下面展开介绍华为仓颉语言的函数初步知识。

函数定义

基本语法:仓颉语言使用func关键字来定义函数,后跟函数名、参数列表、可选的返回值类型以及函数体。其基本语法为:

func 函数名(参数列表): 返回值类型 {

    // 函数体

    return 返回值

}

说明

参数列表:函数可以有 0 个或多个参数,参数分为非命名参数命名参数。非命名参数定义为p: T,如a: Int64;命名参数定义为p!: T,如a!: Int64。形参(定义函数时的参数)规则:

    a.只能为命名参数设置默认值,不能为非命名参数设置默认值。

b.参数列表中可以同时定义非命名参数和命名参数,但是非命名参数只能定义在命名参数之前,命名参数之后不能再出现非命名参数。如:

// 错误示例

func add(a!: Int64, b: Int64): Int64 { // 错误,命名参数 'a' 必须定义在非命名参数 'b' 之后

    return a + b

}

c.函数参数均为不可变变量,在函数定义内不能对其赋值。如:

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

    a = a + b // 错误

    return a

}

返回值类型:函数定义时,返回值类型是可选的,可以显式地定义返回值类型(返回值类型定义在参数列表和函数体之间),也可以不定义返回值类型,交由编译器推导确定。

a.显式声明返回类型

当显式地定义了函数返回值类型时,就要求函数体的类型、函数体中所有 return e 表达式中 e 的类型是返回值类型的子类型。

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

    return a + b

}

若返回值类型不匹配,会报错:

func badAdd(a: Int64, b: Int64): Int64 {

    return (a, b)  // ❌ 错误:元组不能赋给 Int64

}

b.编译器推导返回值类型

在函数定义时如果未显式定义返回值类型,编译器将根据函数体的类型以及函数体中所有的 return 表达式来共同推导出函数的返回值类型。

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

    return a + b  // 编译器推导返回类型为 Int64

}。

返回值类型注意事项

  •  函数的返回值类型并不是任何情况下都可以被推导出来的,如果返回值类型推导失败,编译器会报错。

  •  指定返回类型为 Unit 时,编译器会在函数体中所有可能返回的地方自动插入表达式 return (),使得函数的返回类型总是为 Unit。

func sayHello(): Unit {
    println("Hello!")
    // 自动在末尾插入 return ()
}

func sayHello2(){
    println("Hello!")
    // 自动在末尾插入 return ()
}
sayHello2()函数没有显式声明返回类型 (: Unit),但函数体内的最后一个表达式是 println("Hello!"),这个表达式的类型是 Unit。因此,编译器能够正确推导出该函数的返回类型为 Unit。当函数返回类型为 Unit 时,编译器确实会在函数体末尾自动插入 return () 语句。
 

函数体定义在一对花括号内。函数体中定义了函数被调用时执行的操作,通常包含一系列的变量定义和表达式,也可以包含新的函数定义(即嵌套函数)。

函数定义示例

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

如果函数体是单个表达式,仓颉也支持隐式返回(省略 return 关键字):

func add(a: Int64, b: Int64): Int64 {
    a + b  // 最后一行表达式的值即为返回值
}

函数参数示例

非命名参数 (Non-named Parameters): 即普通参数,调用时按顺序传递。非命名参数必须定义在命名参数之前。例如:

// 定义一个包含两个非命名参数的函数
func multiply(a: Int64, b: Int64): Int64 {
    return a * b
}

main() {
   // 调用函数(必须按顺序传递参数,无需指定参数名)
    let result = multiply(3, 4)  // 正确调用,返回12
    println(result) 
}

命名参数 (Named Parameters): 在参数名后加 !,调用时需指定参数名,并可提供默认值,提高了代码可读性和灵活性。例如:

// 定义包含命名参数的函数(带默认值)
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)                     // 再转回整数
}

注意:

只能为命名参数设置默认值,不能为非命名参数设置默认值。

参数列表中可以同时定义非命名参数和命名参数,但是需要注意的是,非命名参数只能定义在命名参数之前,也就意味着命名参数之后不能再出现非命名参数。例如,下例中 add 函数的参数列表定义是不合法的:

func add(a!: Int64, b: Int64): Int64 { // 错误!
    return a + b
}
 

调用函数规则

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 时可省略 -> 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
}

作用域与遮蔽(Shadow

仓颉参数(形参)作用域为整个函数体,函数参数为不可变(immutable)变量,在函数定义内不能对其赋值,也不能在函数体内重新定义同名变量。例如:

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

main() {
    println(demo(5))  // 输出 6
}

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

示例:局部变量遮盖全局变量:

// 全局变量: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(未被修改)
}

函数类型(Function Type)

仓颉编程语言中,函数是一等公民(first-class citizens),可以作为函数的参数或返回值,也可以赋值给变量。因此函数本身也有类型,称之为函数类型。

函数类型由函数的参数类型和返回类型组成,参数类型和返回类型之间使用 -> 连接。参数类型使用圆括号 () 括起来,可以有 0 个或多个参数,如果参数超过一个,参数类型之间使用逗号(,)分隔。

函数类型是编程中一个比较抽象但极其强大的概念,是函数式编程范式(FP)的核心基石之一。

函数类型的语法非常直观,遵循以下格式:

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

解释一下:

  •  括号 ():里面放置函数的参数类型列表。如果没有参数,就空着 ()。

  •  箭头 ->:连接参数和返回值,读作“返回”。

  •  返回值类型:在箭头后面,指定函数返回的数据类型。

例如:

没参数也要空括号:​() -> Unit

多个参数逗号隔:​(Int, String) -> Bool

返回元组括号包:​(Int, Int) -> (Int, Int)

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

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

例子:

// 【函数定义】

// 程序员写的是具体的实现

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

    return a + b

}

// 【隐式的函数类型】

// 编译器会自动识别出这个函数有一个类型:(Int64, Int64) -> Int64

// 这个类型是“依据函数定义存在的”,程序员没有显式写出 `(Int64, Int64) -> Int64` 这几个字。

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

你完全可以先写出类型,再去找或定义一个符合该类型的函数(甚至用变量、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).作为参数传递给另一个函数

(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
}

运行截图:

函数类型的核心用途

1. 声明函数类型的变量

在仓颉中,声明变量必须显式或通过初始化值来表明类型。

// 定义一个函数
func add(a: Int64, b: Int64): Int64 {
    return a + b
}

main() {
    // 正确声明1: 显式指定变量类型,再赋值函数
    let operation: (Int64, Int64) -> Int64 // 声明一个函数类型的变量
    operation = add // 将函数赋值给变量

    // 正确声明2: 声明的同时初始化(类型由编译器推断)
    let anotherOperation = add // 编译器能推断出anotherOperation的类型是 (Int64, Int64) -> Int64
    
    //现在,operation或anotherOperation就代表了 add 函数
    let result = operation(5, 3)
    println(result) // 输出 8

    let result2 = anotherOperation(5, 3)
    println(result2) // 输出 8
}

2. 作为函数的参数(高阶函数)

// 导入标准库中的集合包
import std.collection.ArrayList // 使用ArrayList

// 高阶函数的参数类型使用函数类型 (Int64) -> String
// 输入和输出使用 ArrayList 类型
func processNumbers(numbers: ArrayList<Int64>, transform: (Int64) -> String): ArrayList<String> {
    // 创建一个新的 ArrayList 来存放结果
    let results = ArrayList<String>()
    
    // 遍历输入的 ArrayList - 使用 for-in 循环
    for(num in numbers) {
        // 调用传入的 transform 函数处理每个元素
        let transformedValue = transform(num)
        // 将结果添加到新的集合中
        results.add(transformedValue)
    }
    return results
}

// 处理行为的函数定义不变
func intToString(num: Int64): String {
    return "Number: ${num}"
}

main() {
    // 使用 ArrayList 而不是原生数组
    let myNumbers = ArrayList<Int64>([1, 2, 3, 4, 5])
    
    // 调用方式完全一样,传递函数名
    let resultList = processNumbers(myNumbers, intToString)
    
    // 遍历结果 ArrayList - 使用 for-in 循环
    for (str in resultList) {
        println(str)
    }
    
    // 同样可以使用 Lambda 表达式
    let squaredList = processNumbers(myNumbers, { n => "Squared: ${n * n}" })
    
    // 遍历 squaredList
    for (str in squaredList) {
        println(str)
    }
}

3. 作为函数的返回值

// 这个函数返回一个 () -> String 类型的函数
func getGreeter(prefix: String): () -> String {
    // 在内部定义一个函数,它捕获了参数 `prefix`
    func greeter(): String {
        return "${prefix}, Hello!" 
    }
    return greeter // 返回这个内部函数
}

main() {
    // getGreeter 返回的是一个函数
    let casualGreet = getGreeter("Hi")
    let formalGreet = getGreeter("Good morning")
    
    // 调用返回的函数
    println(casualGreet()) // 输出: Hi, Hello!
    println(formalGreet()) // 输出: Good morning, Hello!
}

顺便提示,先把函数定义好,再传递函数名。也可可以用Lambda 表达式(匿名函数),下面示例对比:

import std.collection.ArrayList // 使用ArrayList

// 高阶函数的参数类型使用函数类型 (Int64) -> String
// 输入和输出使用 ArrayList 类型
func processNumbers(numbers: ArrayList<Int64>, transform: (Int64) -> String): ArrayList<String> {
    // 创建一个新的 ArrayList 来存放结果
    let results = ArrayList<String>()
    
    // 遍历输入的 ArrayList - 使用 for-in 循环
    for(num in numbers) {
        // 调用传入的 transform 函数处理每个元素
        let transformedValue = transform(num)
        // 将结果添加到新的集合中
        results.add(transformedValue)
    }
    return results
}

main() {
    // 使用 ArrayList 而不是原生数组
    let myNumbers = ArrayList<Int64>([1, 2, 3, 4, 5])

    // ---------1.具名函数(普通写法)------------    
    // 先写一个具名函数
    func multiply10(num: Int64): String {
        return "Value is: ${num * 10}"
    }
    // 把函数名当参数传进去
    let result = processNumbers(myNumbers, multiply10)    
    // 遍历结果
    for(str in result) {
        println(str)
    }

     println("-----------")

    // ---------2.Lambda 表达式(匿名写法)------------ 
    // Lambda 表达式写法
    let result2 = processNumbers(myNumbers, { num => "Value is: ${num * 10}" })    
    // 遍历结果
    for(str in result2) {
        println(str)
    }
}

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

示例:

// 首先定义 showFruitPrice 函数
func showFruitPrice(name: String, price: Int64): Unit {
    println("Fruit: ${name}, Price: ${price}")
}

main() {
    // 1. 全部写名字 —— 合法
    let handler1: (name: String, price: Int64) -> Unit = showFruitPrice

    // 2. 全部不写名字 —— 合法  
    let handler2: (String, Int64) -> Unit = showFruitPrice

    // 3. 混写 —— 非法,编译时报错
    // let handler3: (name: String, Int64) -> Unit   // Error: 必须统一写或统一不写参数名

    // 调用函数
    handler1("apple", 5)   // 正确
    handler2("apple", 5)   // 同样正确
}

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐