函数声明

仓颉编程语言中,函数声明和函数定义的区别是,前者没有函数体。函数声明的语法如下:

functionDeclaration
: functionModifierList? 'func'identifier
typeParameters?
functionParameters
(':' type)?
genericConstraints?
;

函数声明可以出现在抽象类,接口中。

函数重定义

对于非泛型函数,在同一个作用域中,参数类型完全相同的同名函数被视为重定义,将产生一个编译错误。以下几种情况要格外注意:

同名函数即使返回类型不同也构成重定义。
• 同名的泛型与非泛型函数永不构成重定义。
• 在继承时,对于子类中的一个与父类同名且参数类型完全相同的函数,若其返回类型是父类中的版本的子类型,则构成覆盖,也不构成重定义。(这是因为子类与父类作用域不同。)

对于两个泛型函数,如果重命名一个函数的泛型形参后,其非泛型部分与另一个函数的非泛型部分构成重定义,则这两个泛型函数构成重定义。举例如下:

下面这两个泛型函数构成重定义,因为存在一种 [T1 |-> T2] 的重命名(作用到第一个函数上),使得两个函数的非泛型部分构成重定义。

func f<T1>(a: T1) {}func f<T2>(b: T2) {}

下面这两个泛型函数不构成重定义,因为找不到上述的一种重命名。

func f<X,Y>(a:X, b:Y) {}func f<Y,X>(a:X, b:Y) {}

下面的这两个泛型函数构成重定义,因为存在一种 [X |-> Y, Y |-> X] 的重命名使得两个函数的非泛型部分构成重定义。

func f<X,Y>(a:X, b:Y) {}func f<Y,X>(a:Y, b:X) {}

函数类型

函数类型由函数的参数类型和返回类型组成,其中参数类型与返回类型之间用-> 分隔。

functionType:
: '(' (type (, type)*)? ')' '->' type

以下是一些示例:

示例 1:没有参数、返回类型为 Unit

func hello(): Unit { print("Hello!") }// function type: () -> Unit

示例 2:参数类型为 Int32,返回类型为 Unit

func display(a: Int32): Unit { print("${a}") }// function type: (Int32) -> Unit

示例 3:两个参数类型均为 Int32,返回类型为 Int32

func add(a: Int32, b: Int32): Int32 { a + b }// function type: (Int32, Int32) -> Int32

示例 4:参数类型为 (Int32, Int32) -> Int32, Int32 和 Int32,返回类型为 Unit

func printAdd(add: (Int32, Int32) -> Int32, a: Int32, b: Int32): Unit {print("${add(a, b)}")
}
// function type: ((Int32, Int32) -> Int32, Int32, Int32) -> Unit


示例 5:两个参数类型均为 Int32,返回类型为函数类型 (Int32, Int32) -> Int32

func returnAdd(a: Int32, b: Int32): (Int32, Int32) -> Int32 {{a, b => a + b}
}
// function type: (Int32, Int32) -> (Int32, Int32) -> Int32

示例 6:两个参数类型均为 Int32, 返回为一个元组类型为:(Int32, Int32)。

func returnAB(a: Int32, b: Int32): (Int32, Int32) { (a, b) }// function type: (Int32, Int32) -> (Int32, Int32)

函数调用

命名实参

命名实参是指在函数调用时,在实参值前使用 形参名 : 前缀来指定这个实参对应的形参。只有在函数定义时使用! 定义的命名形参,才可以在调用时使用命名实参的语法。

函数调用时,所有的命名形参均必须使用命名实参来传参,否则报错。

在函数调用表达式中,命名实参后不允许出现非命名实参。使用命名实参指定实参值时,可以不需要和形参列表的顺序保持一致。

func add(a!: Int32, b!: Int32): Int32 {a + b
}
var sum1 = add(1, 2) // error
var sum2 = add(a: 1, b: 2) // OK, 3
var sum3 = add(b: 2, a: 1) // OK, 3

函数调用类型检查

本节介绍的是,给定一个调用表达式,对调用表达式所需要进行的类型检查。如果被调用的函数涉及重载,则需要根据函数重载的规则进行类型检查和重载决议。

1、如果函数调用表达式中指定了类型参数,只有类型实参的个数与类型形参的个数相同才可能通过类型检查,即假设函数调用表达式为:f<T1, …, Tm>(A1, …, An),其中给定了 m 个类型实参,则函数类型形参数量须为 m;

open class Base {}
class Sub <: Base {}
func f<X, Y>(a: X, b: Y) {} // f1
func f<X>(a: Base, b: X) {} // f2
f<Base>(Base(), Sub()) // f2 may pass the type checking

根据调用表达式中的实参和调用表达式所在的类型检查上下文中指定的返回类型 R 对函数进行类型检查。

假设函数定义为:

在这里插入图片描述

  1. 如果调用表达式带了类型实参:fi<T1, …, Tp>(A1, …, Ak),那么对于函数 fi 的类型检查规则如下:

a) 类型实参约束检查:类型实参 <T1, ..., Tp> 需要满足函数 fi 的类型约束。

在这里插入图片描述
b) 参数类型检查:将类型实参代入函数 fi 的形参后,满足实参类型 (A1, …, Ak) 是类型实参代入形参后类型的子类型。

在这里插入图片描述
c) 返回类型检查:如果调用表达式的上下文对其有明确类型要求 R,则需要根据返回类型进行类型检查,将类型实参代入函数 fi 的返回类型 Ri 后,满足类型实参代入后的返回类型是 R 的子类型。

在这里插入图片描述
如果调用表达式不带类型实参:f(A1, …, An),那么对于函数 fi 的类型检查规则如下:a) 如果fi是非泛型函数,则按如下规则进行类型检查:

参数类型检查:实参类型 (A1, ..., Ak) 是形参类型的子类型。

在这里插入图片描述
ii. 返回类型检查:如果调用表达式的上下文对其有明确类型要求 R,则检查函数 fi 的返回类型 Ri 是 R 的子类型。

在这里插入图片描述

open class Base {}
class Sub <: Base {}
func f(a: Sub) {1} // f1
func f(a: Base) {Base()} // f2
let x: Base = f(Sub()) // f2 can pass the type checking

b) 如果fi是泛型函数,则按如下规则进行类型检查 :

i. 参数类型检查:存在代换使得实参类型(A1, ..., Ak)是形参类型代换后的类型的子类↪ 型。

在这里插入图片描述
ii. 返回类型检查:如果调用表达式的上下文对其有明确类型要求 R,则需要根据返回类型进行类型检查,将i) 中的代换代入函数 fi 的返回类型 Ri 后,满足代换后的返回类型是 R 的子类型。

在这里插入图片描述
需要注意的是:

  1. 如果函数有缺省值,在类型检查时会补齐缺省值之后再进行类型检查;
  2. 如果函数有命名参数,命名参数的顺序可能会和形参顺序不一致,在类型检查时,命名实参要与名字匹配的命名形参对应。

尾随 Lambda

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

func f(a: Int64, fn: (Int64)->Int64) { fn(a) }
f(1, { i => i * i }) // normal function call
f(1) { i => i * i } // trailing lambda
func g(a!: Int64, fn!: (Int64)->Int64) { fn(a) }
g(a: 1, fn: { i => i * i }) // normal function call
g(a: 1) { i => i * i } // trailing lambda
当函数调用有且只有一个 lambda 实参时,我们还可以省略 (),只写 lambda。func f(fn: (Int64)->Int64) { fn(1) }
f{ i => i * i }

如果尾随 lambda 不包含形参,=> 可以省略。

func f(fn: ()->Int64) { fn() }f{ i * i }

需要注意的是,尾随 lambda 语法只能用在具有 函数名/变量名的函数调用上,并且尾随 lambda 的lambda 表达式只会解释为 函数名/变量名对应的函数的参数。这意味着以下两种调用例子是不合法的:

func f(): (()->Unit)->Unit {
{a => }
}
f() {} // error, the lambda expression is not argument for f.
func g(a: ()->Unit) {}
if (true) { g } else { g } () {} // error, illegal trailing lambda syntax

必须先将以上的表达式赋值给变量,使用变量名调用时才可以使用尾随 lambda 语法。如下面的代码所示:

let f2 = f()
f2 {} // ok
let g2 = if (true) { g } else { g }
g2() {} // ok

普通函数调用和构造函数调用都可以使用这个语法,包含 this() 和 super()。

this(1, { i => i * i } )this(1) { i => i * i }
super(1, { i => i * i } )super(1) { i => i * i }

变长参数

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

变长参数没有特殊的声明语法,只要求函数声明处最后一个非命名参数是 Array 类型。

  1. 变长参数在函数调用时可以使用普通参数列表的形式逐个传入 Array 的多个元素。
  2. 非命名参数中,只有最后一个位置的参数可以使用变长参数。命名参数不能使用这个语法糖。
  3. 变长参数对全局函数、静态成员函数、实例成员函数、局部函数、构造函数、函数变量、lambda、函数调用操作符重载、索引操作符重载的调用都适用,不支持其它操作符重载、compose、pipeline 这几种调用方式。
  4. 变长参数的个数可以是 0 个或以上。
  5. 变长参数只有在函数重载所有情况都不匹配的情况下,才判断是否可以应用语法糖,优先级最低。
func f1(arr: Array<Int64>) {}
func f2(a: Int64, arr: Array<Int64>) {}
func f3(arr: Array<Int64>, a: Int64) {}
func f4(arr1!: Array<Int64>, a!: Int64, arr2!: Array<Int64>) {}

func g() {
let li = [1, 2, 3]
f1(li)
f1(1, 2, 3) // using variable length argument
f1() // using variable length argument
f2(4, li)
f2(4, 1, 2, 3) // using variable length argument
f3(1, 2, 3) // error, Array is not the last parameter
f4(arr1: 1,2,3, a: 2, arr2: 1,2,3) // error, named parameters cannot use↪ variable length argument
}

函数重载决议总是会优先考虑不使用变长参数就能匹配的函数,只有在所有函数都不能匹配,才尝试使用变长参数解析。

当编译器无法决议时会报错。

open class A {
func f(v: Int64): Unit { // f1
}
}
class B <: A {
func f(v: Array<Int64>): Unit { // f2}
}
func p1() {
let x = B()
x.f(1) // call the f1
}
func g<T>(arg: T): Unit { // g1
}
func g(arg: Array<Int64>): Unit { // g2}
func p2() {
g(1) // call the g1
}
func h(arg: Any): Unit { // h1
}
func h(arg: Array<Int64>): Unit { // h2}
func p3() {
h(1) // call the h1
}
Logo

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

更多推荐