4020 字
20 分钟
仓颉文档阅读-开发指南IV: 函数(I) - 函数的定义、调用
NOTE

阅读文档版本:

语言规约 Cangjie-0.53.18-Spec

具体开发指南 Cangjie-LTS-1.0.3

在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证

有条件当然可以直接 配置Canjie-SDK

WARNING

博主在此之前, 基本只接触过C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与C/C++中的相似概念作类比, 见谅

且, 本系列是文档阅读, 而不是仓颉的零基础教学, 所以如果要跟着阅读的话最好有一门编程语言的开发经验

WARNING

在阅读仓颉编程语言的开发指南之前, 已经大概阅读了一遍 仓颉编程语言的语言规约,已经对仓颉编程语言有了一个大概的了解

所以在阅读开发指南时,不会对类似: 类、函数、结构体、接口等解释起来较为复杂名称 做出解释

此样式内容, 表示文档原文内容

函数#

如果你了解过C/C++, 相信对函数并不陌生

定义函数#

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

其中,函数名可以是任意的合法标识符,参数列表定义在一对圆括号内(多个参数间使用逗号分隔),参数列表和函数返回值类型(如果存在)之间使用冒号分隔,函数体定义在一对花括号内

函数定义举例:

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

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

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

仓颉的函数定义语法:

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

参数列表#

一个函数可以拥有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, 命名参数 'a' 必须定义在非命名参数 '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, 重定义 'b'
return a
}

仓颉的非命名参数不允许声明在命名参数之前

且, 仓颉函数的参数均为不可变变量, 即 无法修改形参的值

函数返回值类型#

函数返回值类型是函数被调用后得到的值的类型

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

当显式地定义了函数返回值类型时,就要求函数体的类型(关于如何确定函数体的类型可参见下节函数体)、函数体中所有return e表达式中e的类型是返回值类型的子类型

例如,对于上述add函数,显式地定义了它的返回值类型为Int64;如果将函数体中的return a + b修改为return (a, b),则会因为类型不匹配而报错:

// Error, return 后表达式的类型 与 函数的返回类型不匹配
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
}

注意:

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

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

仓颉函数的返回值类型, 可以显式声明也可以省略, 省略时, 编译器会根据函数体内的上下文进行推导

当显式声明返回值类型时, 函数体内的返回值的类型, 必须要是目标类型或目标类型的子类型

函数体#

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

如下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 expr(expr是一个表达式)

对于return expr,要求expr的类型与函数定义中的返回值类型保持一致

例如,下例中会因为return 100100类型(Int64)和函数foo的返回值类型(String)不同而报错

// Error, 无法将整型字面量转换为“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类型

函数体是函数被调用时, 会执行的操作

函数体内, 是一系列的表达式或声明, 甚至是函数定义(嵌套函数)

在函数被调用时, 函数体内的表达式按顺序、逻辑会一一被执行

函数体内可以通过调用return表达式, 终止函数的执行并返回

可以是return也可以是return expr, expr的类型要满足是函数返回值类型或其子类型

expr值就是此次函数调用的返回值, return等价于return ()

在函数体内定义的变量属于局部变量的一种(如上例中的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)
}

仓颉的函数体也拥有类型, 其类型是函数体类的最后一个”项”的类型

如果是表达式, 那么函数体类型就是表达式的类型

如果是变量定义或函数声明等, 那么函数体类型就是Unit

函数体类型 要为 函数的返回值类型 或 其子类型

如果函数体内没有return表达式, 那么, 函数体内的最后一项的值会自动作为函数的返回值

调用函数#

函数调用的形式为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

仓颉的函数调用 与C/C++ 没有太大差别:

funcname(param1, param2, param3, ...)

传参, 需要按照函数声明的参数类型顺序进行

但仓颉函数还存在命名参数, 命名参数在进行传参时, 必须指定参数名:

func add(a: Int64, b!: Int64) {
return a + b
}
main() {
let sum = add(1, b: 2)
println("1 + 2 = ${sum}")
}

b: 2, b就是形参名, 2是实参值

对于多个命名参数,调用时的传参顺序可以和定义时的参数顺序不同

例如,下例中调用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

仓颉函数, 如果存在多个命名参数, 那么在函数调用进行传参时, 因为需要命名传参, 所以可以不按照顺序进行传参

func add(a!: Int64, b!: Int64) {
return a + b
}
main() {
let sum = add(b: 2, a: 1)
println("1 + 2 = ${sum}")
}

且, 仓颉函数的命名形参是可以存在默认值的, 类似C++函数的参数缺省值, 且只有命名形参可以存在默认值

拥有默认值的命名形参, 在函数调用时可以不用传参, 当然也可以传参

不过, 仓颉函数的命名形参默认值可以不按照顺序声明, 这与C++函数参数的缺省值不同, C++存在缺省值的参数 之后的参数也必须拥有缺省值

仓颉函数, 非命名参数和命名参数共存时, 非命名参数的传参依旧需要保持声明顺序, 命名参数需要在非命名参数传参之后, 再进行传参(此时 命名参数的传参可以不按照顺序)

func add4(a: Int64, b: Int64, c!: Int64, d!: Int64, e!: Int64 = 5 ) {
return a + b + c + d + e
}
main() {
let sum = add4(1, 2, d: 4, c: 3)
println("1 + 2 + 3 + 4 + 5(default) = ${sum}")
}