函数的定义与调用

函数调用

命名参数

当调用一个Kotlin定义的函数时,可以显式地标明一些参数的名称。

/* 集合的格式化输出 */
fun <T> joinToString(
    collection: Collection<T>,
    separator: String, // 分隔符
    prefix: String, // 前缀
    postfix: String // 后缀
): String { ... }
1
2
3
4
5
6
7
val str = joinToString(collection, "", "", "")
1
// 利用命名参数来增强可读性
val str = joinToString(collection, separator = "", prefix = "", postfix = "")
1
2

如果在调用一个函数时,指明了一个函数的名称,为了避免混淆,那它之后的所有参数都需要标明名称。

参数默认值

在Kotlin中,可以在声明函数的时候,指定参数的默认值。

fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
): String { ... }
1
2
3
4
5
6

可以用所有参数来调用这个函数,或者省略掉部分参数。

val str1 = joinToString(collection, ", ", "", "")
val str2 = joinToString(collection)
val str3 = joinToString(collection, "; ")
1
2
3

当使用常规的调用语法时,必须按照函数声明中定义的参数顺序类给定参数,可以省略的只有排在末尾的参数。如果使用命名参数,可以省略中间的一些参数,也可以以你想要的任意顺序只给定你需要的参数。

val str = joinToString(collection, postfix = "]", suffix = "[")
1

注意,参数的默认值是被编码到被调用的函数中,而不是调用的地方。

Java没有参数默认值的概念,当你从Java中调用Kotlin函数的时候,必须显式地指定所有参数值。可以用@JvmOverloads注解Kotlin函数,指示编译器生成Java重载函数,方便Java调用。

@JvmOverloads
fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
): String { ... }
1
2
3
4
5
6
7
String joinToString(Collection<T> collection, String separator, String prefix, String postfix);

String joinToString(Collection<T> collection, String separator, String prefix);

String joinToString(Collection<T> collection, String separator);

String joinToString(Collection<T> collection);
1
2
3
4
5
6
7

因此,在Kotlin中,为函数的参数设置默认值,可以避免创建重载的函数。

可变参数

使用vararg修饰的参数是可变参数,可以接受任意个数的参数。例如,调用listOf函数并传入若干个元素来创建一个列表。

fun listOf<T>(vararg values: T): List<T> { ... }
1
val list = listOf(1, 3, 5, 7)
1

Kotlin的可变参数与Java的区别是:当需要传递给可变参数的实参已经包装在数组中时,Java可按原样传递数组,而Kotlin则要求你显式地解包数组,以便每个数组元素在函数中能作为单独的参数来调用。

val numbers = arrayOf(1, 3, 5, 7) // 创建一个数组
val list = listOf(*numbers) // 使用展开运算符*来解包数组
1
2

中辍调用

函数调用的格式为:目标对象名.函数名(参数),而中辍调用的格式为:目标对象名 函数名 参数

无论是普通函数还是扩展函数,要允许使用中辍调用,需要使用infix修饰符来标记它。例如,Kotlin标准库中的to函数。

infix fun Any.to(other: Any) = Pair(this, other) // 创建一个键值对
1
1.to("one") // 普通调用
1 to "one" // 中辍调用
1
2

顶层函数和属性

顶层函数

在Java中有很多这样的类,它不包含任何的状态或者实例函数,而是仅仅作为一堆静态函数的容器。例如,JDK中的Collections类,实际项目开发中的那些以Util作为后缀命名的工具类。

在Kotlin中,根本就不需要去创建这些无意义的类,可以把这些函数直接放在代码文件的顶层,不从属于任何的类。

/* join.kt */
package strings

fun joinToString(...): String { ... }
1
2
3
4

这些顶层函数依然是包内的成员,如果需要从包外访问它,则需要import

Kotlin中的顶层函数会被编译成一个类的静态函数,而这个类默认为包含该顶层函数的文件的名称。

package strings;

public class JoinKt {
    public static String joinToString(...) { ... }
}
1
2
3
4
5

可在Kotlin文件的开头使用@JvmName注解来修改包含顶层函数的生成的类的名称。

/* join.kt */
@file:JvmName("StringFunctions") // 生成的Java类不再是JoinKt,而是StringFunctions

package strings

fun joinToString(...): String { ... }
1
2
3
4
5
6

顶层属性

和函数一样,属性也可以放在文件的顶层。

顶层属性和其他任何的属性一样,是通过访问器暴露给Java使用。val属性提供gettervar属性提供gettersetter。但注意,顶层属性的字段和访问器都是static

可以使用const val来声明一个类似于Java的public static final的常量属性,而不提供getter。注意,const适用于所有的基本数据类型以及String类型。

/* TopProp.kt */
val name = "daking"
var age = 26
const val UNIX_LINE_SEPARATOR = "\n"
1
2
3
4
/* TopPropKt.java */
public final class TopPropKt {
    @NotNull
    private static final String name = "daking";
    private static int age = 26;
    @NotNull
    public static final String UNIX_LINE_SEPARATOR = "\n";
    
    @NotNull
    public static final String getName() {
        return name;
    }
    
    public static final int getAge() {
        return age;
    }
    
    public static final void setAge(int var0) {
        age = var0;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

扩展函数和属性

扩展函数

可以为现有的类添加函数,这个函数叫做扩展函数,而这个类被称为该扩展函数的接收者类型,且用来调用这个扩展函数的那个对象叫作接收者对象

扩展函数

在扩展函数中,可以使用this访问接收者对象(可省略this)。也可以直接访问接收者类型的其他方法和属性,但不能是私有或受保护的。

可以像调用类的普通成员函数那样去调用扩展函数。

val c = "Kotlin".lastChar()
1

导入扩展函数

扩展函数不会自动地在整个项目范围内生效,需要像其他任何的类或顶层函数一样import

import strings.lastChar

val c = "Kotlin".lastChar()
1
2
3

可以使用关键字as来修改导入的类或者函数名称。

import strings.lastChar as last

val c = "Kotlin".last()
1
2
3

从Java中调用扩展函数

扩展函数其实也是一种顶层函数,它也会被编译成一个静态函数,而包含它的类名默认为Kotlin文件名。但扩展函数会把其接收者类型作为第一个参数。

/* StringUtil.kt */
package strings

fun String.lastChar(): Char = this.get(this.length - 1)
1
2
3
4
package strings;

public class StringUtilKt {
    public static Char lastChar(String str) {
        return str.get(str.length - 1)
    }
}
1
2
3
4
5
6
7
char c = StringUtilKt.lastChar("Java");
1

扩展函数不可重写

因为Kotlin会把扩展函数编译成静态函数,所以扩展函数不可以重写。

如果一个类的成员函数和扩展函数有相同的签名,成员函数往往会被优先使用。

可以给基类和子类都分别定义一个同名的扩展函数。当这个函数被调用时,是由变量的静态类型决定调用基类还是子类的扩展函数,而不是由这个变量的运行时类型决定。

扩展属性

可以为现有的类添加属性,这个属性叫做扩展属性

尽管它们被称为属性,但它们可以没有任何状态,因为没有合适的地方来存储它,不可能给现有的Java对象添加额外的字段。但有时候属性语法比函数语法更便于使用。

var String.lastChar: Char
    get() = this.get(this.length - 1)
    set(value: Char) {
        this.setCharAt(this.length - 1, value)
    }
1
2
3
4
5

扩展属性是没有支持的字段,即没有任何的状态。这是因为没有合适的地方来存储状态值,这导致了:

  • 扩展属性没有默认的getter实现,必须自定义getter函数。

  • 扩展属性不可以进行初始化。

  • 扩展属性的setter要保存在接收者对象的其他字段上。

在Java中访问扩展属性,应该显式地调用它的gettersetter

StringUtilKt.getLastChar("Java");
1

局部函数和扩展

局部函数是指在某个函数的内部定义的函数。例如,将验证代码封装成局部函数。

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user ${user.id}: " +
                "empty $fieldName") // 局部函数可以访问外层函数
        }
    }
    
    validate(user.name, "Name")
    validate(user.address, "Address")
    
    // 验证成功进行保存
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以将局部函数放在扩展函数中。

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user $id: " +
                "empty $fieldName")
        }
    }
    
    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser(user: User) {
    validateBeforeSave()
    
    // 验证成功进行保存
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17