第4章 函数
函数是 Kotlin 的一等公民。这一章讲函数怎么声明、参数怎么传、有哪些 Java 没有的写法。后面第 5 章讲 Lambda 和高阶函数,那是函数作为参数传递的玩法,本章先把函数本身讲清楚。
4.1 基本声明
Kotlin 用 fun 关键字声明函数:
kotlin
// 完整写法:fun 函数名(参数: 类型): 返回类型 { 体 }
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
// 函数体只有一个表达式时,可以省略花括号和 return
fun min(a: Int, b: Int): Int = if (a < b) a else b
// 进一步:返回类型可推断时(表达式函数),可以省略返回类型
fun add(a: Int, b: Int) = a + b
// 无返回值(相当于 Java 的 void)
fun greet(name: String) {
println("你好,$name")
}
注意:Unit 类型相当于 Java 的 void。如果函数不返回有意义的值,编译器自动返回 Unit,不用显式写。
4.2 参数:默认值、命名参数、可变参数
4.2.1 默认参数
kotlin
// 最后一个参数有默认值
fun greet(name: String, prefix: String = "先生/女士") {
println("$prefix $name")
}
greet("张三") // 先生/女士 张三
greet("李四", "教授") // 教授 李四
Java 没有参数默认值。Kotlin 这个特性减少了大量重载方法。
4.2.2 命名参数
调用函数时可以指定参数名:
kotlin
greet(name = "王五", prefix = "医生")
// 顺序可以调换
greet(prefix = "警官", name = "赵六")
参数一多,命名参数能显著提升可读性。调用别人写的函数时,看到 greet("李四", "教授"),你不一定记得第二个参数是什么;写 greet(name = "李四", prefix = "教授") 就一目了然。
4.2.3 可变参数 vararg
kotlin
fun sum(vararg numbers: Int): Int {
var total = 0
for (n in numbers) total += n
return total
}
sum(1, 2, 3) // 6
sum(1, 2, 3, 4, 5) // 15
// 已有的数组展开传参,用 * 前缀
val arr = intArrayOf(1, 2, 3)
sum(*arr) // 6
vararg 对应 Java 的 int... numbers,但 Kotlin 里可以有多个参数,vararg 不必放最后(放最后最常见)。
4.3 函数的位置:顶层、局部、成员
4.3.1 顶层函数
Java 要求所有方法写在类里。Kotlin 允许函数直接放在 .kt 文件顶层:
kotlin
// utils.kt ------ 不需要包一层类
fun parseIntOrNull(s: String): Int? = s.toIntOrNull()
fun main() {
println(parseIntOrNull("123")) // 123
println(parseIntOrNull("abc")) // null
}
这个 parseIntOrNull 编译后会被放进 utils 文件对应的 UtilsKt 类里,作为 public static final 方法。Java 代码可以直接 UtilsKt.parseIntOrNull("123") 调用。
4.3.2 局部函数
函数里面还可以再定义函数:
kotlin
fun processUser(name: String, age: Int) {
// 局部函数:只在 processUser 内部可见
fun validate(): Boolean {
return name.isNotBlank() && age > 0
}
if (validate()) {
println("处理 $name, $age 岁")
} else {
println("参数非法")
}
}
局部函数可以访问外层函数的参数和局部变量,适合把一个大函数里的小步骤拆出来,又不想污染全局命名空间。
4.3.2 局部函数闭包
局部函数不仅能读外层参数,还能读写外层函数的局部变量,哪怕外层函数已经返回------这一点第 5 章讲 Lambda 闭包时还会展开。举个例子,下面这个函数返回一个计数器,内部状态就藏在局部函数里:
kotlin
fun makePrinter(prefix: String): () -> Unit {
var count = 0
// 局部函数:每次调用都让 count 自增并打印
fun printLine(msg: String) {
count++
println("$prefix #$count: $msg")
}
return { printLine("hello") }
}
val p = makePrinter("LOG")
p(); p(); p()
// LOG #1: hello
// LOG #2: hello
// LOG #3: hello
4.3.3 成员函数
写在类里面的函数,和 Java 一样:
kotlin
class Calculator {
fun add(a: Int, b: Int) = a + b // 成员函数
}
Calculator().add(1, 2)
4.4 表达式函数与 Unit
kotlin
// 这个函数没有有意义的返回值
fun log(msg: String): Unit {
println("[LOG] $msg")
}
// 写不写 : Unit 都行,编译器会推断
fun log2(msg: String) {
println("[LOG] $msg")
}
为什么要用 Unit 这个名字?因为 Kotlin 的函数类型系统里,"不返回值"也是一种类型 Unit(它只有一个单例对象),这样高阶函数才能统一处理所有函数。
4.5 尾递归(tailrec)
Kotlin 有一个 tailrec 修饰符,用于优化尾递归。写递归时,如果递归调用是函数的最后一个操作,编译器会把它优化成循环,避免栈溢出:
kotlin
// 普通递归,n 大了会栈溢出
fun factorial(n: Int): Int =
if (n <= 1) 1 else n * factorial(n - 1)
// 尾递归优化版本:把累积器 acc 作为参数传进去
tailrec fun factorialTail(n: Int, acc: Int = 1): Int {
if (n <= 1) return acc
return factorialTail(n - 1, n * acc) // 最后一步是递归调用本身
}
println(factorialTail(10)) // 3628800
tailrec 要求递归调用必须是函数的最后一个操作。像上面 n * factorial(n-1) 这种,乘法在递归结果上做,就不是尾递归,不能加 tailrec。
4.6 本章小结
fun声明函数;表达式函数= expr可省略花括号和返回类型。- 参数默认值、命名参数、
vararg可变参数,配合*展开数组。 - 函数可以放在文件顶层(编译成静态方法)、函数内部(局部函数)、类里(成员函数)。
- 无返回值对应
Unit。 tailrec把符合条件的尾递归编译成循环,避免栈溢出。
动手练习
- 写一个函数
repeatStr(s: String, times: Int = 3, separator: String = " "),把s重复times次用分隔符连接,用命名参数调用三次,每次传不同的参数。 - 用局部函数把一个"计算工资"的函数拆成:基本工资、奖金、税三个内部步骤。
- 写一个尾递归函数计算斐波那契数列第 n 项(提示:需要两个累积参数)。