仓颉入门:函数

函数是一个参数化的代码块,在调用函数时,这些代码块实现特定功能并可以被求值,结合函数参数实现特定范围的代码复用

1. 定义函数

仓颉使用关键字 func 来表示函数定义的开始,func 之后依次是函数名、参数列表、可选的函数返回值类型、函数体。用下面的伪代码表示:


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

函数名可以是任意的合法标识符,下面依次对函数定义中的参数列表、函数返回值类型和函数体作进一步介绍。

1.1 参数列表

一个函数可以拥有 0 个或多个参数,可以将参数列表中的参数分为两类:非命名参数和命名参数。

1.2 非命名参数

非命名参数的定义方式是 p: T,其中 p 表示参数名,T 表示参数 p 的类型,参数名和其类型间使用冒号连接。


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

1.3 命名参数

命名参数还可以设置默认值,通过 p!: T = e 方式将参数 p 的默认值设置为表达式 e 的值。


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

注意
1.只能为命名参数设置默认值,不能为非命名参数设置默认值。
2.非命名参数只能定义在命名参数之前
3.函数参数均为不可变变量,在函数定义内不能对其赋值。

1.4 函数返回值类型

函数返回值类型是函数被调用后得到的值的类型。可以显式地定义返回值类型,也可以不定义返回值类型,交由编译器推导确定。
当显式地定义了函数返回值类型时,就要求函数体的类型、函数体中所有 return e 表达式中 e 的类型是返回值类型的子类型。


func add(a:Int16,b:Int16):Int16{
    if(a-b>0){
        return a
    }else{
        return ''  //error: mismatched types
    }
}

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

1.5 函数体

在函数体内定义的变量属于局部变量的一种,它的作用域从其定义之后开始到函数体结束。
对于一个局部变量,允许在其外层作用域中定义同名变量,并且在此局部变量的作用域内,局部变量会遮盖外层作用域的同名变量。


var s = 10
func add(a:Int16,b:Int16):Int16{
    var s:Int16 = 0
    return s
}
main() {
    println(add(1,2))  // 输出 0
}

2. 调用函数

根据函数定义时参数是非命名参数还是命名参数的差异,函数调用时传实参的方式也有所不同:对于非命名参数,它对应的实参是一个表达式;对于命名参数,它对应的实参需要使用 p: e 的形式

2.1 非命名参数调用


func add(a: Int64, b: Int64) {
    return a + b
}
main() {
    let s = add(10086, 2025)
    println("求和: ${s}")  //求和: 12111
}

2.2 命名参数调用

对于多个命名参数,调用时的传参顺序可以和定义时的参数顺序不同。
对于拥有默认值的命名参数,调用时如果没有传实参,那么此参数将使用默认值作为实参的值。


func add(a: Int64, b!: Int64) {
    return a + b
}
func add2(a!: Int64, b!: Int64) {
    return a + b
}
func add3(a!: Int64=1, b!: Int64=2) {
    return a + b
}
main() {
    let s = add(1, b:2)
    let s2 = add2(a:1, b:2)
    let s3 = add2(b:1,a:2)
    let s4 = add3(a:2)
    let s5 = add3()
    println("一个命名参数:${s}")  //一个命名参数:3
    println("多个命名参数:${s2}")  //一个命名参数:3
    println("多个命名参数:${s3}")  //一个命名参数:3
    println("命名参数有默认值:${s4}")  //一个命名参数:4
    println("命名参数有默认值:${s5}")  //一个命名参数:3
}

3. 函数类型

仓颉编程语言中,函数是一等公民,可以作为函数的参数或返回值,也可以赋值给变量。因此函数本身也有类型,称之为函数类型。
函数类型由函数的参数类型和返回类型组成,参数类型和返回类型之间使用 ->连接。参数类型使用圆括号 () 括起来,可以有 0 个或多个参数,如果参数超过一个,参数类型之间使用逗号(,)分隔。


func display(a: Int64): Unit {  // 类型 (Int64) -> Unit
    println(a)
}
func add(a: Int64, b: Int64): Int64 {  // 类型 (Int64, Int64) -> Int64
    a + b
}
func returnTuple(a: Int64, b: Int64): (Int64, Int64) { //类型 (Int64, Int64) -> (Int64, Int64)
    (a, b)
}

3.1 函数类型的类型参数

可以为函数类型标记显式的类型参数名


func add(a: Int64, b: Int64) {
    println("求和: ${a+b}")
}
main() {
    let add2: (a: Int64, b: Int64) -> Unit
    add2 = add
    add2(11, 10)
}

3.2 函数类型作为参数类型


func add(a: Int64, b: Int64):Int64 {
    return a+b
}
func printAdd(add: (Int64, Int64) -> Int64, a: Int64, b: Int64): Int64 {
    return add(a,b)
}
main() {
    println(printAdd(add,1,2))
}

3.3 函数类型作为返回类型


func add(a: Int64, b: Int64): Int64 {
   return a + b
}
func returnAdd(): (Int64, Int64) -> Int64 {
    return add
}
main() {
    var a: (Int64, Int64) ->  Int64 
    a = add
    println(a(1,2))  //输出 3
}

3.4 函数类型作为变量类型

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


func add(a: Int64, b: Int64): Int64 {
   return a + b
}
main() {
    var a: (Int64, Int64) ->  Int64  =add
    println(a(1,2))  //输出 3
}

4. 嵌套函数

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


func foo() {
    func nestAdd(a: Int64, b: Int64) {
        a + b
    }
    return nestAdd
}
main() {
    let f = foo()
    println(f(1, 2))
}

5. Lambda 表达式

Lambda 表达式是一种匿名函数(即没有函数名的函数),其核心设计目的是在程序中快速定义简短的函数逻辑,无需显式声明函数名称。
Lambda 表达式的语法为如下形式: { 参数列表也可没参数 => 表达式或声明 }。
Lambda 表达式中 不支持声明返回类型,其返回类型总是从上下文中推断出来,若无法推断则报错。

5.1 Lambda 表达式调用


let add = { a: Int64, b: Int64 => a + b } // 定义Lambda 表达式
var display = { => println("Hello") } // 定义Lambda 表达式
var sum1: (Int64, Int64) -> Int64 = { a, b => a + b }
func sum2(a1: (Int64) -> Int64): Int64 {
    a1(1)
}
main() {
    let f = add     // 定义Lambda 表达式
    println(f(2, 2))  //输出4
    display()   //输出 hello
    println(sum1(2,2))  //输出4
    println(sum2({ a2 => a2+1  })) //输出2
    let r1 = { a: Int64, b: Int64 => a + b }(1, 2)
    println(r1)   // 输出3 立即调用
    var r2 = { x: Int64,y:Int64 => println(x+y) }
    r2(1,2)    // 输出3 先赋值给一个变量,再调用
}

6. 闭包

在嵌套函数的基础上,内层函数捕获了外层局部变量。函数和捕获的变量一起被称为一个闭包。

6.1 变量捕获

1.函数的参数缺省值中访问了本函数之外定义的局部变量;
2.函数内访问了本函数之外定义的局部变量;
3.class/struct 内定义的不是成员函数的函数访问了实例成员变量或 this。


func add(): (Int64) -> Int64 {
    let num: Int64 = 10
    func sum(a: Int64) {
        return a + num   //闭包 add,捕获了 let 声明的局部变量 num
    }
    sum
}
main() {
    println(add()(1))
}

为了防止捕获了 var 声明变量的闭包逃逸,这类闭包只能被调用,不能作为一等公民使用,包括不能赋值给变量,不能作为实参或返回值使用,不能直接将闭包的名字作为表达式使用。


func f() {
    var x = 1
    func g() {
        println(x)  //捕获了一个可变变量
    }
    // g //捕获可变变量的函数需要直接调用
    g() // 正常
}
main() {
    println(f())
}

7. 函数调用语法糖

7.1 尾随 lambda

当函数最后一个形参是函数类型,并且函数调用对应的实参是 lambda 时,可以使用尾随 lambda 语法,将 lambda 放在函数调用的尾部,圆括号外面。


func add(a: Bool, fn: (Int64) -> Int64) {
     if(a) {
        fn(1)
    } else {
        0
    }
}
func add2(fn2:(Int64)->Int64){
    fn2(2)
}
main() {
    println(add(true,{b:Int64=>b+1}))
    println(add(true){
        b:Int64=>b+1
    })
    println(add2{a=>a*2})  //当函数调用有且只有一个 lambda 实参时,还可以省略 (),只写 lambda。
}

7.2 Flow表达式

流操作符包括两种:表示数据流向的中缀操作符 |> (称为 pipeline)和表示函数组合的中缀操作符 ~> (称为 composition)。

7.3 pipeline 表达式

当需要对输入数据做一系列的处理时,可以使用 pipeline 表达式来简化描述。pipeline 表达式的语法形式如下:e1 |> e2。等价于如下形式的语法糖:let v = e1; e2(v) 。
其中 e2 是函数类型的表达式,e1 的类型是 e2 的参数类型的子类型。函数依次执行,前面函数的执行结果是后面函数参数的子类型。

func inc(x: Array): Ar

func inc(x: Array<Int64>): Array<Int64> { // 将数组中每个元素的值增加 “1”
    let s = x.size
    var i = 0
    for (e in x where i < s) {
        x[i] = e + 1
        i++
    }
    x
}
func sum(y: Array<Int64>): Int64 { // 获取数组中元素的总和
    var s = 0
    for (j in y) {
        s += j
    }
    s
}
main() {
    let arr: Array<Int64> = [1, 3, 5]
    let res = arr |> inc |> sum
    // inc [2,4,6] sum = 2+4+6
    println(res)  //输出12
}

7.4 composition 表达式

composition 表达式表示两个单参函数的组合。composition 表达式语法为 f ~> g,等价于 { x => g(f(x)) }。
其中 f,g 均为只有一个参数的函数类型的表达式。
f 和 g 组合,则要求 f(x) 的返回类型是 g(...) 的参数类型的子类型。


func add1(a:Int64):Int64{
    return a+1
}
func add2(a2:Int64):Int64{
    return a2+2
}
main() {
    let a = add1~>add2
    let a1 =add1~>add1~>add1 //多次执行
    println(a(1))  //输出4
    println(a1(1))  //输出4
}

7.5 变长参数

变长参数是一种特殊的函数调用语法糖。当形参最后一个非命名参数是 Array 类型时,实参中对应位置可以直接传入参数序列代替 Array 字面量(参数个数可以是 0 个或多个)。


func sum(arr: Array<Int64>) {
    var total = 0
    for (x in arr) {
        total += x
    }
    return total
}
main() {
    println(sum())
    println(sum(1, 2, 3))
}

注意
只有最后一个非命名参数可以作为变长参数,命名参数不能使用这个语法糖。

8. 函数重载

在仓颉编程语言中,如果一个作用域中,一个函数名对应多个函数定义,这种现象称为函数重载。

8.1 函数重载定义

1.函数名相同,函数参数不同(是指参数个数不同,或者参数个数相同但参数类型不同)的两个函数构成重载。
2.对于两个同名泛型函数,如果重命名一个函数的泛型形参后(使泛型参数顺序相同),其非泛型部分与另一个函数的非泛型部分函数参数不同,则两个函数构成重载,否则这两个泛型函数构成重复定义错误(类型变元的约束不参与判断)。


func f1<X, Y>(a: X, b: Y) {}
// func f1<Y, X>(a: X, b: Y) {} //属于重载 参数类型顺序相同
// func f1<Y, X>(b: X, a: Y) {} //属于重载 参数类型顺序相同
// func f1<X, Y>(a: Y, b: X) {}  //属于重载
func f1<Y, X>(a: Y, b: X) {}  //重载冲突

3.同一个类内的两个构造函数参数不同,构成重载。
4.同一个类内的主构造函数和 init 构造函数参数不同,构成重载。
5.如果子类中存在与父类同名的函数,并且函数的参数类型不同,则构成函数重载。

8.2 函数重载决议

函数调用时,所有可被调用的函数构成候选集,候选集中有多个函数,究竟选择候选集中哪个函数,需要进行函数重载决议,有如下规则:
1.优先选择作用域级别高的作用域内的函数。
2.如果作用域级别相对最高的仍有多个函数,则需要选择实参最匹配的函数。
3.子类和父类认为是同一作用域。

9 操作符重载

如果希望在某个类型上支持此类型默认不支持的操作符,可以使用操作符重载实现。
如果需要在某个类型上重载某个操作符,可以通过为类型定义一个函数名为此操作符的函数的方式实现,这样,在该类型的实例使用该操作符时,就会自动调用此操作符函数。
操作符函数定义与普通函数定义相似,区别如下:
1.定义操作符函数时需要在 func关键字前面添加 operator修饰符;
2.操作符函数的参数个数需要匹配对应操作符的要求
3.操作符函数只能定义在 class、interface、struct、enum 和 extend 中;
4.操作符函数具有实例成员函数的语义,所以禁止使用 static修饰符;
5.操作符函数不能为泛型函数。

9.1 操作符函数重载定义和使用

定义操作符函数有两种方式:
对于可以直接包含函数定义的类型 (包括 struct、enum、class 和 interface ),可以直接在其内部定义操作符函数的方式实现操作符的重载。
使用 extend的方式为其添加操作符函数,从而实现操作符在这些类型上的重载。对于无法直接包含函数定义的类型(是指除 struct、class、enum 和 interface 之外其他的类型)或无法改变其实现的类型,比如第三方定义的 struct、class、enum 和 interface,只能采用这种方式。
操作符函数对参数类型的约定如下:
对于一元操作符,操作符函数没有参数,对返回值的类型没有要求。
对于二元操作符,操作符函数只有一个参数,对返回值的类型没有要求。


open 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 +(right: Point): Point {
        Point(this.x + right.x, this.y + right.y)
    }
}
main() {
    let p1 = Point(8, 24)
    let p2 = -p1      // p2 = Point(-8, -24)
    let p3 = p1 + p2  // p3 = Point(0, 0)
    println('p3.x:${p3.x} p3.y:${p3.y}') //p3.x:0 p3.y:0
}

3.索引操作符([])分为取值 let a = arr[i] 和赋值 arr[i] = a 两种形式。除 enum 外的不可变类型不支持重载索引操作符赋值形式。
4.函数调用操作符(())重载函数,输入参数和返回值类型可以是任意类型。

10 const 函数和常量求值

常量求值允许某些特定形式的表达式在编译时求值,可以减少程序运行时需要的计算。

10.1 const上下文与const表达式

const 上下文是指 const 变量初始化表达式,这些表达式始终在编译时求值。因此需要对 const 上下文中允许的表达式加以限制,避免修改全局状态、I/O 等副作用,确保其可以在编译时求值。

10.2 const 函数

const 函数是一类特殊的函数,这些函数具备了可以在编译时求值的能力。在 const 上下文中调用这种函数时,这些函数会在编译时执行计算。而在其他非 const 上下文,const 函数会和普通函数一样在运行时执行。

10.3 const init

如果一个 struct 或 class 定义了 const 构造器,那么这个 struct/class 实例可以用在 const 表达式中。const init 与 const 函数的区别是 const init 内允许对实例成员变量进行赋值(需要使用赋值表达式)。

鸿蒙学习地址

posted @ 2025-12-12 11:43  leon_teacher  阅读(0)  评论(0)    收藏  举报