函数

函数定义

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

函数定义举例:

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

上例中定义了一个名为 add 的函数,其参数列表由两个 Int64 类型的参数 ab 组成,函数返回值类型为 Int64,函数体中将 ab 相加并返回。

下面依次对函数定义中的参数列表、函数返回值类型和函数体作进一步介绍。

参数列表

一个函数可以拥有 0 个或多个参数,这些参数均定义在函数的参数列表中。根据函数调用时是否需要给定参数名,可以将参数列表中的参数分为两类:非命名参数和命名参数。

非命名参数的定义方式是 p: T,其中 p 表示参数名,T 表示参数 p 的类型,参数名和其类型间使用冒号连接。例如,上例中 add 函数的两个参数 ab 均为非命名参数。

命名参数的定义方式是 p!: T,与非命名参数的不同是在参数名 p 之后多了一个 !。可以将上例中 add 函数的两个非命名参数修改为命名参数,如下所示:

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

命名参数还可以设置默认值,通过 p!: T = e 方式将参数 p 的默认值设置为表达式 e 的值。例如,可以将上述 add 函数的两个参数的默认值都设置为 1

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

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

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

func add(a!: Int64, b: Int64): Int64 { // Error: named parameter 'a' must be defined after non-named parameter 'b'
    return a + b
}

非命名参数和命名参数的主要差异在于调用时的不同,具体可参见下文[函数调用]中的介绍。

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

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

函数参数作用域从定义处起至函数体结束:

func add(a: Int64, b: Int64): Int64 {
    var a_ = a // OK
    var b = b  // Error: redefinition of declaration 'b'
    return a
}

函数返回值类型

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

当显式地定义了函数返回值类型时,就要求函数体的类型(关于如何确定函数体的类型可参见下节[函数体])、函数体中所有 return e 表达式中 e 的类型是返回值类型的子类型。例如,对于上述 add 函数,显式地定义了它的返回值类型为 Int64,如果将函数体中的 return a + b 修改为 return (a, b),则会因为类型不匹配而报错:

// Error: the type of the expression after return does not match the return type of the function
func add(a: Int64, b: Int64): Int64 {
    return (a, b)
}

在函数定义时如果未显式定义返回值类型,编译器将根据函数体的类型以及函数体中所有的 return 表达式来共同推导出函数的返回值类型。例如,下例中 add 函数的返回值类型虽然被省略,但编译器可以根据 return a + b 推导出 add 函数的返回值类型是 Int64:

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

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

函数体

函数体中定义了函数被调用时执行的操作,通常包含一系列的变量定义和表达式,也可以包含新的函数定义(即嵌套函数)。如下 add 函数的函数体中首先定义了 Int64 类型的变量 r(初始值为 0),接着将 a + b 的值赋值给 r,最后将 r 的值返回:

func add(a: Int64, b: Int64) {
    var r = 0
    r = a + b
    return r
}

在函数体的任意位置都可以使用 return 表达式来终止函数的执行并返回。return 表达式有两种形式:returnreturn exprexpr 是一个表达式)。

对于 return expr,要求 expr 的类型与函数定义中的返回值类型保持一致。例如,下例中会因为 return 100100 类型(Int64)和函数 foo 的返回值类型(String)不同而报错。

// error: cannot convert an integer literal to type 'Struct-String'
func foo(): String {
    return 100
}

对于 return,其等价于 return (),所以要求函数的返回值类型为 Unit

func add(a: Int64, b: Int64) {
    var r = 0
    r = a + b
    return r
}

func foo(): Unit {
    add(1, 2)
    return
}

注:return 表达式作为一个整体,其类型并不由后面跟随的表达式决定,而是 Nothing 类型。

在函数体内定义的变量属于局部变量的一种(如上例中的 r 变量),它的作用域从其定义之后开始到函数体结束。

对于一个局部变量,允许在其外层作用域中定义同名变量,并且在此局部变量的作用域内,局部变量会“遮盖”外层作用域的同名变量。例如:

let r = 0
func add(a: Int64, b: Int64) {
    var r = 0
    r = a + b
    return r
}

上例中,add 函数之前定义了 Int64 类型的全局变量 r,同时 add 函数体内定义了同名的局部变量 r,那么在函数体内,所有使用变量 r 的地方(如 r = a + b),用到的将是局部变量 r,即(在函数体内)局部变量 r “遮盖”了全局变量 r

上节中我们提到函数体也是有类型的,函数体的类型是函数体内最后一“项”的类型:若最后一项为表达式,则函数体的类型是此表达式的类型,若最后一项为变量定义或函数声明,或函数体为空,则函数体的类型为 Unit。例如:

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

上例中,因为函数体的最后一“项”是 Int64 类型的表达式(即 a + b),所以函数体的类型也是 Int64,与函数定义的返回值类型相匹配。又如,下例中函数体的最后一项是 print 函数调用,所以函数体的类型是 Unit,同样与函数定义的返回值类型相匹配:

func foo(): Unit {
    let s = "Hello"
    print(s)
}

函数调用

函数调用的形式为 f(arg1, arg2, ..., argn)。其中,f 是要调用的函数的名字,arg1argnn 个调用时的参数(称为实参),要求每个实参的类型必须是对应参数类型的子类型。实参可以有 0 个或多个,当实参个数为 0 时,调用方式为 f()

根据函数定义时参数是非命名参数还是命名参数的不同,函数调用时传实参的方式也有所不同:对于非命名参数,它对应的实参是一个表达式,对于命名参数,它对应的实参需要使用 p: e 的形式,其中 p 是命名参数的名字,e 是表达式(即传递给参数 p 的值)。

非命名参数调用举例:

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

main() {
    let x = 1
    let y = 2
    let r = add(x, y)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 3

命名参数调用举例:

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

main() {
    let x = 1
    let y = 2
    let r = add(x, b: y)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 3

对于多个命名参数,调用时的传参顺序可以和定义时的参数顺序不同。例如,下例中调用 add 函数时 b 可以出现在 a 之前:

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

main() {
    let x = 1
    let y = 2
    let r = add(b: y, a: x)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 3

对于拥有默认值的命名参数,调用时如果没有传实参,那么此参数将使用默认值作为实参的值。例如,下例中调用 add 函数时没有为参数 b 传实参,那么参数 b 的值等于其定义时的默认值 2

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

main() {
    let x = 1
    let r = add(x)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 3

对于拥有默认值的命名参数,调用时也可以为其传递新的实参,此时命名参数的值等于新的实参的值,即定义时的默认值将失效。例如,下例中调用 add 函数时为参数 b 传了新的实参值 20,那么参数 b 的值就等于 20

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

main() {
    let x = 1
    let r = add(x, b: 20)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 21

函数是一等公民

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

函数类型

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

例如:

func hello(): Unit {
    println("Hello!")
}

上述示例定义了一个函数,函数名为 hello,其类型是 () -> Unit,表示该函数没有参数,返回类型为 Unit

以下给出另一些示例:

  • 示例:函数名为 display,其类型是 (Int64) -> Unit,表示该函数有一个参数,参数类型为 Int64,返回类型为 Unit

    func display(a: Int64): Unit {
        println(a)
    }
    
  • 示例:函数名为 add,其类型是 (Int64, Int64) -> Int64,表示该函数有两个参数,两个参数类型均为 Int64,返回类型为 Int64

    func add(a: Int64, b: Int64): Int64 {
        a + b
    }
    
  • 示例:函数名为 returnTuple,其类型是 (Int64, Int64) -> (Int64, Int64),两个参数类型均为 Int64, 返回类型为元组类型:(Int64, Int64)

    func returnTuple(a: Int64, b: Int64): (Int64, Int64) {
        (a, b)
    }
    

函数类型的类型参数

可以为函数类型标记显式的类型参数名,下面例子中的 nameprice 就是 类型参数名

main() {
    let fruitPriceHandler: (name: String, price: Int64) -> Unit
    fruitPriceHandler = {n, p => println("fruit: ${n} price: ${p} yuan")}
    fruitPriceHandler("banana", 10)
}

另外对于一个函数类型,只允许统一写类型参数名,或者统一不写类型参数名,不能交替存在。

let handler: (name: String, Int64) -> Int64   // error

函数类型作为参数类型

  • 示例:函数名为 printAdd,其类型是 ((Int64, Int64) -> Int64, Int64, Int64) -> Unit,表示该函数有三个参数,参数类型分别为函数类型 (Int64, Int64) -> Int64 和两个 Int64,返回类型为 Unit

    func printAdd(add: (Int64, Int64) -> Int64, a: Int64, b: Int64): Unit {
        println(add(a, b))
    }
    

函数类型作为返回类型

函数类型可以作为另一个函数的返回类型。

如下示例中,函数名为 returnAdd,其类型是 () -> (Int64, Int64) -> Int64,表示该函数无参数,返回类型为函数类型 (Int64, Int64) -> Int64。注意,-> 是右结合的。

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

func returnAdd(): (Int64, Int64) -> Int64 {
    add
}

main() {
    var a = returnAdd()
    println(a(1,2))
}

函数类型作为变量类型

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

func add(p1: Int64, p2: Int64): Int64 {
    p1 + p2
}

let f: (Int64, Int64) -> Int64 = add

上述示例中,函数名是 add,其类型为 (Int64, Int64) -> Int64。变量 f 的类型与 add 类型相同, add 被用来初始化 f

若一个函数在当前作用域中被重载(见[函数重载])了,那么直接使用该函数名作为表达式可能产生歧义,如果产生歧义编译器会报错,例如:

func add(i: Int64, j: Int64) {
    i + j
}

func add(i: Float64, j: Float64) {
    i + j
}

main() {
    var f = add   // Error: ambiguous function 'add'
    var plus: (Int64, Int64) -> Int64 = add  // OK
}

嵌套函数

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

示例,函数 foo 内定义了一个嵌套函数 nestAdd,可以在 foo 内调用该嵌套函数 nestAdd,也可以将嵌套函数 nestAdd 作为返回值返回,在 foo 外对其进行调用:

func foo() {
    func nestAdd(a: Int64, b: Int64) {
        a + b + 3
    }

    println(nestAdd(1, 2))  // 6

    return nestAdd
}

main() {
    let f = foo()
    let x = f(1, 2)
    println("result: ${x}")
}

程序会输出:

6
result: 6

Lambda 表达式

Lambda 表达式定义

Lambda 表达式的语法为如下形式:

{ p1: T1, ..., pn: Tn => expressions | declarations }

其中,=> 之前为参数列表,多个参数之间使用 , 分隔,每个参数名和参数类型之间使用 : 分隔。=> 之前也可以没有参数。=> 之后为 lambda 表达式体,是一组表达式或声明序列。Lambda 表达式的参数名的作用域与函数的相同,为 lambda 表达式的函数体部分,其作用域级别可视为与 lambda 表达式的函数体内定义的变量等同。

let f1 = { a: Int64, b: Int64 => a + b }

var display = { => println("Hello") }   // Parameterless lambda expression.

Lambda 表达式不管有没有参数,都不可以省略 =>,除非其作为尾随 lambda。例如:

var display = { => println("Hello") }

func f2(lam: () -> Unit) { }
let f2Res = f2{ println("World") } // OK to omit the =>

Lambda 表达式中参数的类型标注可缺省。以下情形中,若参数类型省略,编译器会尝试进行类型推断,当编译器无法推断出类型时会编译报错:

  • Lambda 表达式赋值给变量时,其参数类型根据变量类型推断;
  • Lambda 表达式作为函数调用表达式的实参使用时,其参数类型根据函数的形参类型推断。
// The parameter types are inferred from the type of the variable sum1
var sum1: (Int64, Int64) -> Int64 = { a, b => a + b }

var sum2: (Int64, Int64) -> Int64 = { a: Int64, b => a + b }

func f(a1: (Int64) -> Int64): Int64 {
    a1(1)
}

main(): Int64 {
    // The parameter type of lambda is inferred from the type of function f
    f({ a2 => a2 + 10 })
}

Lambda 表达式中不支持声明返回类型,其返回类型总是从上下文中推断出来,若无法推断则报错。

  • 若上下文明确指定了 lambda 表达式的返回类型,则其返回类型为上下文指定的类型。

    • Lambda 表达式赋值给变量时,其返回类型根据变量类型推断返回类型:
    let f: () -> Unit = { ... }
    
    • Lambda 表达式作为参数使用时,其返回类型根据使用处所在的函数调用的形参类型推断:
    func f(a1: (Int64) -> Int64): Int64 {
        a1(1)
    }
    
    main(): Int64 {
        f({ a2: Int64 => a2 + 10 })
    }
    
    • Lambda 表达式作为返回值使用时,其返回类型根据使用处所在函数的返回类型推断:
    func f(): (Int64) -> Int64 {
        { a: Int64 => a }
    }
    
  • 若上下文中类型未明确,与推导函数的返回值类型类似,编译器会根据 lambda 表达式体中所有 return 表达式 'return xxx' 中 xxx 的类型,以及 lambda 表达式体的类型,来共同推导出 lambda 表达式的返回类型。

    => 右侧的内容与普通函数体的规则一样。

    let sum1 = { a: Int64, b: Int64 => a + b }
    
  • => 的右侧为空,返回类型为 Unit

    let f = { => }
    

Lambda 表达式调用

Lambda 表达式支持立即调用,例如:

let r1 = { a: Int64, b: Int64 => a + b }(1, 2) // r1 = 3
let r2 = { => 123 }()                          // r2 = 123

Lambda 表达式也可以赋值给一个变量,使用变量名进行调用,例如:

func f() {
    var g = { x: Int64 => println("x = ${x}") }
    g(2)
}

闭包

一个函数或 lambda 从定义它的静态作用域中捕获了变量,函数或 lambda 和捕获的变量一起被称为一个闭包,这样即使脱离了闭包定义所在的作用域,闭包也能正常运行。

函数或 lambda 的定义中对于以下几种变量的访问,称为变量捕获:

  • 函数的参数缺省值中访问了本函数之外定义的局部变量;

  • 函数或 lambda 内访问了本函数或本 lambda 之外定义的局部变量;

  • class/struct 内定义的不是成员函数的函数或 lambda 访问了实例成员变量或 this;

以下情形的变量访问不是变量捕获:

  • 对定义在本函数或本 lambda 内的局部变量的访问;

  • 对本函数或本 lambda 的形参的访问;

  • 对全局变量和静态成员变量的访问;

  • 对实例成员变量在实例成员函数或属性中的访问。由于实例成员函数或属性将 this 作为参数传入,在实例成员函数或属性内通过 this 访问所有实例成员变量。

变量的捕获发生在闭包定义时,因此变量捕获有以下规则:

  • 被捕获的变量必须在闭包定义时可见,否则编译报错;

  • 被捕获的变量必须在闭包定义时已经完成初始化,否则编译报错;

示例 1:闭包 add,捕获了 let 声明的局部变量 num,之后通过返回值返回到 num 定义的作用域之外,调用 add 时仍可正常访问 num

func returnAddNum(): (Int64) -> Int64 {
    let num: Int64 = 10

    func add(a: Int64) {
        return a + num
    }
    add
}

main() {
    let f = returnAddNum()
    println(f(10))
}

程序输出的结果为:

20

示例 2:捕获的变量必须在闭包定义时可见

func f() {
    let x = 99
    func f1() {
        println(x)
    }
    let f2 = { =>
        println(y)      // Error: cannot capture 'y' which is not defined yet
    }
    let y = 88
    f1()          // Print 99.
    f2()
}

示例 3:捕获的变量必须在闭包定义前完成初始化

func f() {
    let x: Int64
    func f1() {
        println(x)    // Error: x is not initialized yet.
    }
    x = 99
    f1()
}

如果捕获的变量是引用类型,可修改其可变实例成员变量的值。

class C {
    public var num: Int64 = 0
}

func returnIncrementer(): () -> Unit {
    let c: C = C()

    func incrementer() {
        c.num++
    }

    incrementer
}

main() {
    let f = returnIncrementer()
    f() // c.num increases by 1
}

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

func f() {
    var x = 1
    let y = 2

    func g() {
        println(x)  // OK, captured a mutable variable.
    }
    let b = g  // Error, g cannot be assigned to a variable

    g  // Error, g cannot be used as an expression
    g()  // OK, g can be invoked

    g  // Error, g cannot be used as a return value.
}

需要注意的是,捕获具有传递性,如果一个函数 f 调用了捕获 var 变量的函数 g,且存在 g 捕获的 var 变量不在函数 f 内定义,那么函数 f 同样捕获了 var 变量,此时,f 也不能作为一等公民使用。

以下示例中,g 捕获了 var 声明的变量 xf 调用了 g,且 g 捕获的 x 不在 f 内定义,f 同样不能作为一等公民使用:

func h(){
    var x = 1

    func g() {  x }   // captured a mutable variable

    func f() {
        g()      // invoked g
    }
    return f // error
}

以下示例中,g 捕获了 var 声明的变量 xf 调用了 g。但 g 捕获的 xf 内定义,f 没有捕获其它 var 声明的变量。因此,f 仍作为一等公民使用:

func h(){
    func f() {
        var x = 1
        func g() { x }   // captured a mutable variable

        g()
    }
    return f // ok
}

静态成员变量和全局变量的访问,不属于变量捕获,因此访问了 var 修饰的全局变量、静态成员变量的函数或 lambda 仍可作为一等公民使用。

class C {
    static public var a: Int32 = 0
    static public func foo() {
        a++       // OK
        return a
    }
}

var globalV1 = 0

func countGlobalV1() {
    globalV1++
    C.a = 99
    let g = C.foo  // OK
}

func g(){
    let f = countGlobalV1 // OK
    f()
}

函数调用语法糖

尾随 lambda

尾随 lambda 可以使函数的调用看起来像是语言内置的语法一样,增加语言的可扩展性。

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

例如,下例中我们定义了一个 myIf 函数,它的第一个参数是 Bool 类型,第二个参数是函数类型。当第一个参数的值为 true 时,返回第二个参数调用后的值,否则返回 0。调用 myIf 时可以像普通函数一样调用,也可以使用尾随 lambda 的方式调用。

func myIf(a: Bool, fn: () -> Int64) {
    if(a) {
        fn()
    } else {
        0
    }
}

func test() {
    myIf(true, { => 100 }) // General function call

    myIf(true) {        // Trailing closure call
        100
    }
}

当函数调用有且只有一个 lambda 实参时,我们还可以省略 (),只写 lambda。

示例:

func f(fn: (Int64) -> Int64) { fn(1) }

func test() {
    f { i => i * i }
}

Flow 表达式

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

Pipeline 表达式

当需要对输入数据做一系列的处理时,可以使用 pipeline 表达式来简化描述。 pipeline 表达式的语法形式如下:

e1 |> e2

等价于如下形式的语法糖:

let v = e1; e2(v)

其中 e2 是函数类型的表达式,e1 的类型是 e2 的参数类型的子类型;

示例:

func inc(x: Array<Int64>): Array<Int64> { // Increasing the value of each element in the array by '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 { // Get the sum of elements in the array.
    var s = 0
    for (j in y) {
        s += j
    }
    s
}

let arr: Array<Int64> = Array<Int64>([1, 3, 5])
let res = arr |> inc |> sum // res = 12

Composition 表达式

composition 表达式表示两个单参函数的组合。composition 表达式语法如下:

f ~> g

等价于如下形式:

{ x => g(f(x)) }

其中 fg 均为只有一个参数的函数类型的表达式。

fg 组合,则要求 f(x) 的返回类型是 g(...) 的参数类型的子类型。

示例 1:

func f(x: Int64): Float64 {
    Float64(x)
}
func g(x: Float64): Float64 {
    x
}

var fg = f ~> g // The same as { x: Int64 => g(f(x)) }

示例 2:

func f(x: Int64): Float64 {
    Float64(x)
}

let lambdaComp = ({x: Int64 => x}) ~> f // The same as { x: Int64 => f({x: Int64 => x}(x)) }

示例 3:

func h1<T>(x: T): T { x }
func h2<T>(x: T): T { x }
var hh = h1<Int64> ~> h2<Int64> // The same as { x: Int64 => h2<Int64>(h1<Int64>(x)) }

注:表达式 f ~> g 中,会先对 f 求值,然后对 g 求值,最后才会进行函数的组合。

需要注意的是,流操作符不能与无默认值的命名形参函数直接一同使用,这是因为无默认值的命名形参函数必须给出命名实参才可以调用。例如:

func f(a!: Int64): Unit {}

var a = 1 |> f  // error

如果需要使用,用户可以通过 lambda 表达式传入 f 函数的命名实参:

func f(a!: Int64): Unit {}

var x = 1 |>  { x: Int64 => f(a: x) } // ok

由于相同的原因,当 f 的参数有默认值时,直接与流运算符一起使用也是错误的,例如:

func f(a!: Int64 = 2): Unit {}

var a = 1 |> f // error

但是当命名形参都存在默认值时,不需要给出命名实参也可以调用该函数,函数仅需要传入非命名形参,那么这种函数是可以同流运算符一起使用的,例如:

func f(a: Int64, b!: Int64 = 2): Unit {}

var a = 1 |> f  // ok

当然,如果想要在调用f时,为参数 b 传入其他参数,那么也需要借助 lambda 表达式:

func f(a: Int64, b!: Int64 = 2): Unit {}

var a = 1 |> {x: Int64 => f(x,  b: 3)}  // ok

变长参数

变长参数是一种特殊的函数调用语法糖。当形参最后一个非命名参数是 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))
}

程序输出:

0
6

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

func length(arr!: Array<Int64>) {
    return arr.size
}

main() {
    println(length())        // error: expected 1 argument, found 0
    println(length(1, 2, 3)) // error: expected 1 argument, found 3
}

变长参数可以出现在全局函数、静态成员函数、实例成员函数、局部函数、构造函数、函数变量、lambda、函数调用操作符重载、索引操作符重载的调用处。不支持其他操作符重载、compose、pipeline 这几种调用方式。示例如下:

class Counter {
    var total = 0
    init(data: Array<Int64>) { total = data.size }
    operator func ()(data: Array<Int64>) { total += data.size }
}

main() {
    let counter = Counter(1, 2)
    println(counter.total)
    counter(3, 4, 5)
    println(counter.total)
}

程序输出:

2
5

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

func f<T>(x: T) where T <: ToString {
    println("item: ${x}")
}

func f(arr: Array<Int64>) {
    println("array: ${arr}")
}

main() {
    f()
    f(1)
    f(1, 2)
}

程序输出:

array: []
item: 1
array: [1, 2]

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

func f(arr: Array<Int64>) { arr.size }
func f(first: Int64, arr: Array<Int64>) { first + arr.size }

main() {
    println(f(1, 2, 3)) // error
}

函数重载

函数重载定义

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

  • 函数名相同,函数参数不同(是指参数个数不同,或者参数个数相同但参数类型不同)的两个函数构成重载。示例如下:
// Scenario 1
func f(a: Int64): Unit {
}

func f(a: Float64): Unit {
}

func f(a: Int64, b: Float64): Unit {
}
  • 对于两个同名泛型函数,如果重命名一个函数的泛型形参后,其非泛型部分与另一个函数的非泛型部分函数参数不同,则两个函数构成重载,否则这两个泛型函数构成重复定义错误(类型变元的约束不参与判断)。示例如下:
interface I1{}
interface I2{}

func f1<X, Y>(a: X, b: Y) {}
func f1<Y, X>(a: X, b: Y) {} // Ok: after rename generic type parameter, it will be 'func f1<X, Y>(a: Y, b: X)'

func f2<T>(a: T) where T <: I1 {}
func f2<T>(a: T) where T <: I2 {} // Error: not overloading
  • 同一个类内的两个构造函数参数不同,构成重载。示例如下:
// Scenario 2
class C {
    var a: Int64
    var b: Float64

    public init(a: Int64, b: Float64) {
        this.a = a
        this.b = b
    }

    public init(a: Int64) {
        b = 0.0
        this.a = a
    }
}
  • 同一个类内的主构造函数和 init 构造函数参数不同,构成重载(认为主构造函数和 init 函数具有相同的名字)。示例如下:
// Scenario 3
class C {
    C(var a!: Int64, var b!: Float64) {
        this.a = a
        this.b = b
    }

    public init(a: Int64) {
        b = 0.0
        this.a = a
    }
}
  • 两个函数定义在不同的作用域,在两个函数可见的作用域中构成重载。示例如下:
// Scenario 4
func f(a: Int64): Unit {
}

func g() {
    func f(a: Float64): Unit {
    }
}
  • 两个函数分别定义在父类和子类中,在两个函数可见的作用域中构成重载。示例如下:
// Scenario 5
open class Base {
    public func f(a: Int64): Unit {
    }
}

class Sub <: Base {
    public func f(a: Float64): Unit {
    }
}

只允许函数声明引入的函数重载,但是以下情形不构成重载,不构成重载的两个名字不能定义或声明在同一个作用域内:

  • class、interface、struct 类型的静态成员函数和实例成员函数之间不能重载
  • enum 类型的 constructor、静态成员函数和实例成员函数之间不能重载

如下示例,两个变量均为函数类型且函数参数类型不同,但由于它们不是函数声明所以不能重载,如下示例将编译报错(重定义错):

main() {
    var f: (Int64) -> Unit
    var f: (Float64) -> Unit
}

如下示例,虽然变量 f 为函数类型,但由于变量和函数之间不能同名,如下示例将编译报错(重定义错):

main() {
    var f: (Int64) -> Unit

    func f(a: Float64): Unit {   // Error: functions and variables cannot have the same name.
    }
}

如下示例,静态成员函数 f 与实例成员函数 f 的参数类型不同,但由于类内静态成员函数和实例成员函数之间不能重载,如下示例将编译报错:

class C {
    public static func f(a: Int64): Unit {
    }
    public func f(a: Float64): Unit {
    }
}

函数重载决议

函数调用时,所有可被调用的函数(是指当前作用域可见且能通过类型检查的函数)构成候选集,候选集中有多个函数,究竟选择候选集中哪个函数,需要进行函数重载决议,有如下规则:

  • 优先选择作用域级别高的作用域内的函数。在嵌套的表达式或函数中,越是内层作用域级别越高。

    如下示例中在 inner 函数体内调用 g(Sub()) 时,候选集包括 inner 函数内定义的函数 ginner 函数外定义的函数 g,函数决议选择作用域级别更高的 inner 函数内定义的函数 g

    open class Base {}
    class Sub <: Base {}
    
    func outer() {
        func g(a: Sub) {
            print("1")
        }
    
        func inner() {
            func g(a: Base) {
                print("2")
            }
    
            g(Sub())   // Output: 2
        }
    }
    
  • 如果作用域级别相对最高的仍有多个函数,则需要选择最匹配的函数(对于函数 f 和 g 以及给定的实参,如果 f 可以被调用时 g 也总是可以被调用的,但反之不然,则我们称 f 比 g 更匹配)。如果不存在唯一最匹配的函数,则报错。

    如下示例中,两个函数 g 定义在同一作用域,选择更匹配的函数 g(a: Sub): Unit

    open class Base {}
    class Sub <: Base {}
    
    func outer() {
        func g(a: Sub) {
            print("1")
        }
        func g(a: Base) {
            print("2")
        }
    
        g(Sub())   // Output: 1
    
    }
    
  • 子类和父类认为是同一作用域。如下示例中,一个函数 g 定义在父类中,另一个函数 g 定义在子类中,在调用 s.g(Sub()) 时,两个函数 g 当成同一作用域级别决议,则选择更匹配的父类中定义的函数 g(a: Sub): Unit

    open class Base {
        public func g(a: Sub) { print("1") }
    }
    
    class Sub <: Base {
        public func g(a: Base) {
            print("2")
        }
    }
    
    func outer() {
        let s: Sub = Sub()
        s.g(Sub())   // Output: 1
    }
    

操作符重载

如果希望在某个类型上支持此类型默认不支持的操作符,可以使用操作符重载实现。

如果需要在某个类型上重载某个操作符,可以通过为类型定义一个函数名为此操作符的函数的方式实现,这样,在该类型的实例使用该操作符时,就会自动调用此操作符函数。

操作符函数定义与普通函数定义相似,区别如下:

  • 定义操作符函数时需要在 func 关键字前面添加 operator 修饰符;
  • 操作符函数的参数个数需要匹配对应操作符的要求(详见附录[操作符]);
  • 操作符函数只能定义在 class、interface、struct、enum 和 extend 中;
  • 操作符函数具有实例成员函数的语义,所以禁止使用 static 修饰符;
  • 操作符函数不能为泛型函数。

另外,需要注意:

  • 被重载后的操作符不改变它们固有的优先级和结合性(详见附录[操作符])。

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

定义操作符函数有两种方式:

  1. 对于可以直接包含函数定义的类型 (包括 structenumclassinterface ),可以直接在其内部定义操作符函数的方式实现操作符的重载。
  2. 使用 extend 的方式为其添加操作符函数,从而实现操作符在这些类型上的重载。对于无法直接包含函数定义的类型(是指除 structclassenuminterface 之外其他的类型)或无法改变其实现的类型,比如第三方定义的 structclassenuminterface,只能采用这种方式(参见[扩展的定义]);

操作符函数对参数类型的约定如下:

  1. 对于一元操作符,操作符函数没有参数,对返回值的类型没有要求。

  2. 对于二元操作符,操作符函数只有一个参数,对返回值的类型没有要求。

    如下示例中介绍了一元操作符和二元操作符的定义和使用:

    - 实现对一个 Point 实例中两个成员变量 xy 取负值,然后返回一个新的 Point 对象,+ 实现对两个 Point 实例中两个成员变量 xy 分别求和,然后返回一个新的 Point 对象。

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

    接下来,就可以在 Point 的实例上直接使用一元 - 操作符和二元 + 操作符:

    main() {
        let p1 = Point(8, 24)
        let p2 = -p1      // p2 = Point(-8, -24)
        let p3 = p1 + p2  // p3 = Point(0, 0)
    }
    
  3. 索引操作符([])分为取值 let a = arr[i] 和赋值 arr[i] = a 两种形式,它们通过是否存在特殊的命名参数 value 来区分不同的重载。索引操作符重载不要求同时重载两种形式,可以只重载赋值不重载取值,反之亦可。

    索引操作符取值形式 [] 内的参数序列对应操作符重载的非命名参数,可以是 1 个或多个,可以是任意类型。不可以有其它命名参数。返回类型可以是任意类型。

    class A {
        operator func [](arg1: Int64, arg2: String): Int64 {
            return 0
        }
    }
    
    func f() {
        let a = A()
        let b: Int64 = a[1, "2"]
        // b == 0
    }
    

    索引操作符赋值形式 [] 内的参数序列对应操作符重载的非命名参数,可以是 1 个或多个,可以是任意类型。= 右侧的表达式对应操作符重载的命名参数,有且只能有一个命名参数,该命名参数的名称必须是 value, 不能有默认值,value 可以是任意类型。返回类型必须是 Unit 类型。

    需要注意的是,value 只是一种特殊的标记,在索引操作符赋值时并不需要使用命名参数的形式调用。

    class A {
        operator func [](arg1: Int64, arg2: String, value!: Int64): Unit {
            return
        }
    }
    
    func f() {
        let a = A()
        a[1, "2"] = 0
    }
    

    特别的,除 enum 外的不可变类型不支持重载索引操作符赋值形式。

  4. 函数调用操作符(())重载函数,输入参数和返回值类型可以是任意类型。示例如下:

    open class A {
        public init() {}
    
        public operator func ()(): Unit {}
    }
    
    func test1() {
        let a = A() // ok, A() is call the constructor of A.
        a() // ok, a() is to call the operator () overloading function.
    }
    

    不能使用 thissuper 调用 () 操作符重载函数。示例如下:

    open class A {
        public init() {}
        public init(x: Int64) {
            this() // ok, this() calls the constructor of A.
        }
    
        public operator func ()(): Unit {}
    
        public func foo() {
            this()  // error, this() calls the constructor of A.
            super() // error
        }
    }
    
    class B <: A {
        public init() {
            super() // ok, super()  calls the constuctor of the super class.
        }
    
        public func goo() {
            super() // error
        }
    }
    

    对于枚举类型,当构造器形式和 () 操作符重载函数形式都满足时,优先匹配构造器形式。示例如下:

    enum E {
        Y | X | X(Int64)
    
        public operator func ()(p: Int64) {}
        public operator func ()(p: Float64) {}
    }
    
    main() {
        let e = X(1) // ok, X(1) is to call the constructor X(Int64).
        X(1.0) // ok, X(1.0) is to call the operator () overloading function.
        let e1 = X
        e1(1) // ok, e1(1) is to call the operator () overloading function.
        Y(1) // oK, Y(1) is to call the operator () overloading function.
    }
    

可以被重载的操作符

下表列出了所有可以被重载的操作符(优先级从高到低):

OperatorDescription
()Function call
[]Indexing
!NOT
-Negative
**Power
*Multiply
/Divide
%Remainder
+Add
-Subtract
<<Bitwise left shift
>>Bitwise right shift
<Less than
<=Less than or equal
>Greater than
>=Greater than or equal
==Equal
!=Not equal
&Bitwise AND
^Bitwise XOR
|Bitwise OR

需要注意的是:

  1. 一旦在某个类型上重载了除关系操作符(<<=>>===!=)之外的其他二元操作符,并且操作符函数的返回类型与左操作数的类型一致或是其子类型,那么此类型支持对应的复合赋值操作符。当操作符函数的返回类型与左操作数的类型不一致且不是其子类型时,在使用对应的复合赋值符号时将报类型不匹配错误;
  2. 仓颉编程语言不支持自定义操作符,即不允许定义除上表中所列 operator 之外的其他操作符函数。
  3. 对于类型 T, 如果 T 已经默认支持了上述若干可重载操作符,那么通过扩展的方式再次为其实现同签名的操作符函数时将报重定义错误。例如,为数值类型重载其已支持的同签名算术操作符、位操作符或关系操作符等操作符时,为 Char 重载同签名的关系操作符时,为 Bool 类型重载同签名的逻辑操作符、判等或不等操作符时,等等这些情况,均会报重定义错。

Mut 函数

Struct 类型是值类型,其实例成员函数无法修改实例本身。例如,下例中,成员函数 g 中不能修改成员变量 i 的值。

struct Foo {
    var i = 0

    public func g() {
        i += 1  // Error: the value of a instance member variable cannot be modified in an instance member function
    }
}

Mut 函数是一种可以修改 struct 实例本身的特殊的实例成员函数。在 mut 函数内部,this 的语义是特殊的,这种 this 拥有原地修改字段的能力。

只允许在 interface、struct 和 struct 的扩展内定义 mut 函数(class 是引用类型,实例成员函数不需要加 mut 也可以修改实例成员变量,所以禁止在 class 中定义 mut 函数)。

Mut 函数定义

Mut 函数与普通的实例成员函数相比,多一个 mut 关键字来修饰。

例如,下例中在函数 g 之前增加 mut 修饰符之后,即可在函数体内修改成员变量 i 的值。

struct Foo {
    var i = 0

    public mut func g() {
        i += 1  // ok
    }
}

mut 只能修饰实例成员函数,不能修饰静态成员函数。

struct A {
    public mut func f(): Unit {} // ok
    public mut operator func +(rhs: A): A { // ok
        A()
    }
    public mut static func g(): Unit {} // Error: static member functions cannot be modified with 'mut'
}

Mut 函数中的 this 不能被捕获,也不能作为表达式。不能在 mut 函数中对 struct 的实例成员变量进行捕获。

示例:

struct Foo {
    var i = 0

    public mut func f(): Foo {
        let f1 = { => this } // Error: 'this' in mut functions cannot be captured
        let f2 = { => this.i = 2 } // Error: instance member variables in mut functions cannot be captured
        let f3 = { => this.i } // Error: instance member variables in mut functions cannot be captured
        let f4 = { => i } // Error: instance member variables in mut functions cannot be captured
        this // Error: 'this' in mut functions cannot be used as expressions
    }
}

接口中的 mut 函数

接口中的实例成员函数,也可以使用 mut 修饰。

struct 类型在实现 interface 的函数时必须保持一样的 mut 修饰。struct 以外的类型实现 interface 的函数时不能使用 mut 修饰。

示例:

interface I {
    mut func f1(): Unit
    func f2(): Unit
}

struct A <: I {
    public mut func f1(): Unit {} // Ok: as in the interface, the 'mut' modifier is used
    public func f2(): Unit {} // Ok: as in the interface, the 'mut' modifier is not used
}

struct B <: I {
    public func f1(): Unit {} // Error: 'f1' is modified with 'mut' in interface, but not in struct
    public mut func f2(): Unit {} // Error: 'f2' is not modified with 'mut' in interface, but did in struct
}

class C <: I {
    public func f1(): Unit {} // ok
    public func f2(): Unit {} // ok
}

struct 的实例赋值给 interface 类型时是拷贝语义,因此 interfacemut 函数并不能修改 struct 实例的值。

示例:

interface I {
    mut func f(): Unit
}
struct Foo <: I {
    public var v = 0
    public mut func f(): Unit {
        v += 1
    }
}
main() {
    var a = Foo()
    var b: I = a  
    b.f()  // Calling 'f' via 'b' cannot modify the value of 'a'
    println(a.v) // 0
}

程序输出结果为:

0

Mut 函数的使用限制

因为 struct 是值类型,所以如果一个变量是 struct 类型且使用 let 声明,那么不能通过这个变量访问该类型的 mut 函数。

示例:

interface I {
    mut func f(): Unit
}
struct Foo <: I {
    public var i = 0
    public mut func f(): Unit {
        i += 1
    }
}
main() {
    let a = Foo()
    a.f() // Error: 'a' is of type struct and is declared with 'let', the 'mut' function cannot be accessed via 'a'
    var b = Foo()
    b.f() // ok
    let c: I = Foo()
    c.f() // ok
}

为避免逃逸,如果一个变量的类型是 struct 类型,那么这个变量不能将该类型使用 mut 修饰的函数作为一等公民来使用,只能调用这些 mut 函数。

示例:

interface I {
    mut func f(): Unit
}

struct Foo <: I {
    var i = 0

    public mut func f(): Unit {
        i += 1
    }
}

main() {
    var a = Foo()
    var fn = a.f // Error: mut function 'f' of 'a' cannot be used as a first class citizen.
    var b: I = Foo()
    fn = b.f // ok
}

为避免逃逸,非 mut 的实例成员函数(包括 lambda 表达式)不能直接访问所在类型的 mut 函数,反之可以。

示例:

struct Foo {
    var i = 0

    public mut func f(): Unit {
        i += 1
        g() // ok
    }

    public func g(): Unit {
        f() // Error: mut functions cannot be invoked in non-mut functions
    }
}

interface I {
    mut func f(): Unit {
        g() // ok
    }

    func g(): Unit {
        f() // Error: mut functions cannot be invoked in non-mut functions
    }
}