# Kotlin教程 - 5 函数

函数也可以称之为方法、功能,我们可以将一段实现特定功能的代码封装为一个函数。

为什么需要封装为函数呢?

举个栗子:

给出两个点的坐标,计算出两个点的距离。

import kotlin.math.sqrt

fun main() {

    val ax = 1.0    // 第一个点的坐标
    val ay = 1.0

    val bx = 2.0    // 第二个点的坐标
    val by = 2.0

    // 求两个点的距离,sqrt是开平方
    val distance = sqrt((ax - bx) * (ax - bx) + (ay - by) * (ay - by))

    println("distance:$distance");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如果在一个地方需要计算两个点的距离,就在一个地方写了这个功能的代码,100个、1000个地方需要这个功能,那就需要写1000遍。万一这个功能需要修改,修改1000个地方不得崩溃。

所以我们可以将这个功能封装为一个函数,想要使用到这个功能的时候,直接调用这个函数即可。

所以通过使用函数,提高代码的复用性,减少重复代码,提高开发效率。

我们之前已经使用了很多内置的函数,例如println()等,现在我们主要学习如何自定义函数。

# 5.1 函数的定义

函数定义的语法:

fun 函数名(参数列表): 返回值类型 {
  函数体
  return 返回值
}
1
2
3
4
  • fun 关键字用于定义函数;
  • 函数名,必须遵循标识符的命名规则,建议首字母小写的驼峰规则;
  • 参数列表,包括参数名和参数类型,多个参数之间用逗号分隔。
  • 返回值类型,指定函数返回的数据类型;
  • 函数体,包含了实际的代码逻辑;
  • 使用 return 语句返回函数的结果,如果没有返回值,return 可以省略。
  • 参数可以缺省;
  • 注意:return语句后面不能再有其他语句。

函数调用的语法:

函数名(参数列表)
1

举个栗子:

下面定义一个函数,计算两个数的和,然后调用该函数。

import kotlin.math.sqrt

/**
 * 定义一个函数,计算两个点的距离,参数是两个点的坐标
 */
fun calculateDistance(ax: Double, ay: Double, bx: Double, by: Double): Double {
    val distance = sqrt((ax - bx) * (ax - bx) + (ay - by) * (ay - by))
    return distance
}

fun main() {
    val ax = 1.0    // 第一个点的坐标
    val ay = 1.0

    val bx = 2.0    // 第二个点的坐标
    val by = 2.0

    // 调用函数calculateDistance,传入参数,计算两个坐标的距离
    val distance = calculateDistance(ax, ay, bx, by)
    println("distance:$distance");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

首先定义了 calculateDistance 函数,函数接收4个参数,也就是两个点的坐标,在函数内,通过坐标计算出距离,然后通过 return 语句返回。

然后在 main 函数中,调用了 calculateDistance 函数,在调用的时候,传递了4个参数,同时定义了一个 distance 常量接收返回的数值。

定义函数时,函数名后面的参数叫形式参数(形参),函数的参数个数不限,使用逗号分隔;

调用函数的时候,传递的是实际参数(实参),表示函数执行的时候的参数值。

函数可以没有参数,如果有参数,参数必须指定类型。

也可以没有返回值,没有返回值的时候,返回值类型可以省略。

举个栗子:

/**
 * 定义sayHello函数,只是打印了一句字符串
 */
fun sayHello() {
    println("Hello")
}

fun main() {
    // 调用sayHello函数
    sayHello()
}
1
2
3
4
5
6
7
8
9
10
11

上面定义了一个 sayHello 函数,函数没有参数,没有返回值,没有返回值类型。

# 5.2 函数的参数

# 1 默认参数

默认参数为形参提供默认值,那么在调用函数的时候,如果不传递参数,则使用默认参数值。

举个栗子:

fun sayHello(name: String, age: Int = 3, gender: String = "男") {
    println("Helo, 我是 $name,我 $age 岁了, 我是 $gender 人")
}

fun main() {
    sayHello("Shabi", 8, "男") // 调用函数
    sayHello("Niubi")      // 也可以不传递参数
}
1
2
3
4
5
6
7
8

指定了默认参数,如果不传递参数,那么参数会使用指定的默认值。

上面传递参数的时候是通过位置传递参数的,也就是说在调用函数的时候,第一个参数是传递给函数的第一个参数,第二个传递给第二个,依此类推,这叫位置传参。

这样的话,没有默认值的参数需要放在前面,这样有默认值的才能不传递参数。

举个栗子:

fun sayHello(age: Int = 3, gender: String = "男", name: String) {
    println("Helo, 我是 $name,我 $age 岁了, 我是 $gender 人")
}
1
2
3

因为 name 参数没有默认值,所以必须传值给 name,但是 name 参数在第三个位置,所以前两个位置的参数也必须传递值,不能省略。如果将 name 放到第一个位置,后面两个参数才可以不传值。

如果不想被位置约束,可以使用下面的命名参数来传值。

# 2 命名参数

命名参数可以不按顺序传递参数,按照参数的名称来传递

举个栗子:

fun sayHello(age: Int = 3, gender: String = "男", name: String) {
    println("Helo, 我是 $name,我 $age 岁了, 我是 $gender 人")
}

fun main() {
    sayHello(name="Doubi", gender = "女")	// 通过名称传递参数
    // sayHello(gender = "女")      // name必须传递,因为name没有默认值
}
1
2
3
4
5
6
7
8

命名参数主要是针对调用而言的,调用的时候,根据名称来传递即可。

# 3 可变数量参数

可变数量参数允许定义接收不定数量的相同类型参数的函数。可变数量参数使用 vararg 关键字来定义。

举个栗子:

/**
 * 定义一个可变数量参数的函数
 */
fun printNumbers(vararg numbers: Int) {
    for (num in numbers) {
        println(num)
    }

    println(numbers[0])     // 取出第一个参数
    println(numbers[1])     // 取出第二个参数
}

fun main() {
    // 调用带有可变数量参数的函数,参数的个数没有限制
    printNumbers(1, 2, 3)
    printNumbers(1, 2, 3, 4, 5)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在上面定义了一个可变数量参数的函数 printNumbers,参数 numbers 其实是一个数组(后面在讲解,就是里面装了多个相同类型的数值),可以使用 for 循环遍历参数,也可以使用 下标 来取出指定位置的参数。(下标从0开始)。

需要注意,一个函数只能有一个可变数量的参数,并且可变数量参数必须是函数的最后一个参数

# 4 特殊字符的函数名

我们之前说函数名必须遵守标识符命名规则,但是因为 Kotlin 和 Java 是可以相互调用的,但是 Kolin 和 Java 中的关键字不是完全相同的,所以如果使用 Kotlin 来调用 Java,但是在 Java 中合法的函数名在 Kotlin 中是关键字,那么 Kotlin 中就无法调用了。

这个时候可以使用反引号(键盘上1左边的按键)将其括起来,这样可以创建具有非标准命名的函数或变量。

举个栗子:

/**
 * 定义了一个特殊字符的函数名函数
 */
fun `when`() {
    println("调用when函数")
}

fun main() {
    `when`()			// 调用函数
}
1
2
3
4
5
6
7
8
9
10

在 Kotlin 中 when 是关键字,不能单独用来做标识符,但是可以时候用反引号括起来,这样就可以作为函数名了,调用的时候,也是需要使用反引号的。

甚至还可以使用汉字来定义函数名:

fun `定义了一个被人看见可能会被打的函数名`() {
    println("被人打了别怪我")
}

fun main() {
    `定义了一个被人看见可能会被打的函数名`()		// 调用函数
}
1
2
3
4
5
6
7

有些公司为了保密,将函数名都定义为数字,然后通过查阅文档才能知道这个函数的功能,起到保密的作用,当然这样做的很少很少,基本不会用,知道就好,那么可以使用数字来定义函数。

fun `10086`() {
    println("我是中国移动")
}

fun main() {
    `10086`()				// 调用函数
}
1
2
3
4
5
6
7

# 5 函数的重载

什么是函数的重载?

函数的重载就是函数的方法名一样,但是参数不一样,注意不包括返回值。

举个栗子:

fun sayHello() {
    println("没有参数")
}

fun sayHello(name: Int) {
    println("有一个整型参数")
}

fun sayHello(name: String) {
    println("有一个字符串参数")
}

fun sayHello(name: String, age: Int) {
    println("多个参数")
}

fun main() {
    sayHello()
    sayHello(12)
    sayHello("Doubi")
    sayHello("Doubi", 12)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

上面的几个函数,方法名一样,但是参数不一样,这就是函数的重载。编译器会根据传递的参数类型和数量来选择正确的函数。

当有默认参数的时候,看看下面的代码:

fun sayHello() {
    println("没有参数")
}

fun sayHello(name: Int = 12) {
    println("有一个整形参数")
}

fun sayHello(name: String = "Doubi") {
    println("有一个字符串参数")
}

fun main() {
    sayHello()		// 没有传递参数,会调用哪一个函数?
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

调用的时候没有传递参数,会调用 没有参数 的函数。

如果将没有参数的函数删掉呢?

fun sayHello(name: Int = 12) {
    println("有一个字符串参数")
}

fun sayHello(name: String = "Doubi") {
    println("有一个字符串参数")
}

fun main() {
    // sayHello()			// 报错
}
1
2
3
4
5
6
7
8
9
10
11

此时代码会报错,因为 sayHello() 不知道调用哪个函数了,因为上面两个函数都可以调用。

# 5.3 函数的返回值

# 1 无返回值类型

在前面已经讲解了,如果函数没有返回值,返回值的类型是可以省略的。

那么如果不省略,没有返回值的时候,返回值类型是什么呢?

在Java中是void,在Kotlin中是 Unit

举个栗子:

/**
 * 没有返回值的函数
 */
fun sayHello() {
    println("Hello World")
}

fun main() {
    println(sayHello())
}
1
2
3
4
5
6
7
8
9
10

执行结果:

Hello World kotlin.Unit

函数没有返回值,打印的结果是 kotlin.Unit

当然,没有返回值的时候,我们也可以显式的定义返回值类型,如下:

/**
 * 没有返回值的函数
 */
fun sayHello(): Unit {
    println("Hello World")
}

fun main() {
    sayHello();
}
1
2
3
4
5
6
7
8
9
10

上面定义了一个没有返回值的函数 sayHello,返回值的类型是 Unit,不过一般都是省略不写了。

# 2 单表达式函数

如果函数体只包含单个表达式,并且编译器可以根据表达式的类型自动推断返回值类型,那么可以省略返回值类型和大括号 {}

举个栗子:

fun add(a: Int, b: Int) = a + b

fun main() {
    println(add(1, 2))  // 调用函数并打印结果
}
1
2
3
4
5

上面的 add 函数的函数体只有一句表达式,那么可以省略返回值类型和大括号 {}

单个表达式都可以,因为 println() 函数没有返回值,所以下面的函数返回值类型是 Unit

fun sayHello() = println("Hello Doubi!")        // 打印Hello Doubi

fun main() {
    sayHello()
}
1
2
3
4
5

# 5.4 函数的嵌套调用

函数的嵌套调用就是一个函数可以调用另外一个函数,另外一个函数还可以继续调用其他的函数,依此类推。

举个栗子:

下面定义了2个函数,funA() 和 funB(),并在main()函数中调用了funA() ,然后在funA()中调用了 funB()。

fun funB() {
    println("----b")
}

fun funA() {
    println("----a1")
    funB()
    println("----a2")
}

fun main() {
    funA()
}
1
2
3
4
5
6
7
8
9
10
11
12
13

执行结果:

----a1 ----b ----a2

我们会发现在 funA() 中调用 funB() 后,funB() 执行完成,重新回到了 funA() 继续执行。

# 5.5 变量的作用域

变量的作用域就是在哪里可以使用这个变量。不可能我们随便定义了一个变量,哪里都可以使用。

变量主要分类两类:局部变量和全局变量。

变量的作用域是根据代码的结构 {} 来决定作用域范围的,优先使用自己作用域中的变量,如果没有找到,则一层层向外查找。

# 1 局部变量

局部变量就是在函数内部定义的变量,这样变量只能在变量内部使用。

举个例子:

fun funA() {
    val num = 3
    print(num)
}

fun main() {
    // print(num); // 无法访问到funA()内定义的变量num
}
1
2
3
4
5
6
7
8

因为变量 numfunA() 函数内部定义,所以在 main 函数无法访问。

局部变量当函数调用完成,就被销毁释放了。

# 2 全局变量

全局变量就是在函数内、外都内调用的变量。

举个栗子:

var a = 1 // 定于全局变量a
var b = 2 // 定于全局变量b

fun main() {
    val a = 5
    run {           // 创建了一个临时局部作用域
        val a = 10
        println(a) // 优先在所在的{}内查找变量
    }
    println(a) // 优先在所在的{}内查找变量,即main()函数内查找a
    println(b) // 优先在所在的{}内查找变量,main函数内没找到,则继续向外寻找
}
1
2
3
4
5
6
7
8
9
10
11
12

val a = 10; 只在它所在的 {} 内有效,{} 外是无法访问的。

执行结果:

10

5

2

# 5.6 函数的递归

什么是函数的递归?

函数的递归,就是在一个函数内部,又调用了这个函数自己,这个就是函数的递归。

举个例子:

fun funA() {
    print("----")
    funA()
}

fun main() {
    funA()
}
1
2
3
4
5
6
7
8

上面的函数,执行 funA(),然后在 funA() 内部又调用了自己,那么会重新又调用了 funA() 函数,然后又调用了自己,这样就会一直无限调用,变成了无限循环调用,执行的时候很快就报Stack Overflow的错误,栈溢出。

所以函数的递归有时候是很危险的,很容易无限调用,造成栈溢出,程序崩溃。所以函数的递归调用一定要注意结束或跳出递归的条件。

例如我们写一个用递归求阶乘的函数:

fun factorial(num: Int): Int {
    // 求阶乘
    if (num <= 1) {
        return 1
    } else {
        return num * factorial(num - 1)
    }
}

fun main() {
    val num = 5
    val result = factorial(num)
    print("${num}的阶乘为:${result}")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

以 5 举例,求 5 的阶乘,调用了 factorial函数 ,则计算 5 乘以 4 的阶乘,然后求 4 的阶乘,重新调用了 factorial函数 ,然后计算 4 乘以 3 的阶乘,一次类推,一直得到1的阶乘,然后向上返回。

递归函数一定得有结束的条件,否则就会无限递归导致栈溢出错误。