5.8.3 扩展

原文链接: https://kotlinlang.org/docs/extensions.html

5.8.3 扩展

Kotlin 的扩展让你无需使用继承或 Decorator 这类设计模式,就能为类或接口添加新功能。在处理无法直接修改的第三方库时,它们非常有用。创建之后,你可以像调用原来类或接口的成员一样调用这些扩展。

最常见的扩展形式是扩展函数和扩展属性。

重要的是,扩展不会修改它所扩展的类或接口。定义扩展时,你并没有添加新成员,只是让新函数可以用同样的语法调用,或让新属性可以用同样的语法访问。

接收者

扩展总是在某个接收者上调用。接收者的类型必须与被扩展的类或接口相同。要使用扩展,请在前面写上接收者,后跟 . 以及函数或属性名。

例如,标准库中的 .appendLine() 扩展函数扩展了 StringBuilder 类。因此在这个例子中,接收者是一个 StringBuilder 实例,接收者类型是 StringBuilder:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun main() {
    // builder 是 StringBuilder 的实例
    val builder = StringBuilder()
        // 在 builder 上调用 .appendLine() 扩展函数
        .appendLine("Hello")
        .appendLine()
        .appendLine("World")
    println(builder.toString())
    // Hello
    //
    // World
}

扩展函数

在创建自己的扩展函数之前,先看看你想要的功能是否已经在 Kotlin 标准库中提供。标准库为以下场景提供了许多有用的扩展函数:

要创建自己的扩展函数,请在函数名前加上接收者类型和 .。在这个例子中,.truncate() 函数扩展了 String 类,因此接收者类型是 String:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun String.truncate(maxLength: Int): String {
    return if (this.length <= maxLength) this else take(maxLength - 3) + "..."
}

fun main() {
    val shortUsername = "KotlinFan42"
    val longUsername = "JetBrainsLoverForever"

    println("Short username: ${shortUsername.truncate(15)}")
    // KotlinFan42
    println("Long username:  ${longUsername.truncate(15)}")
    // JetBrainsLov...
}

.truncate() 函数会把调用它的任何字符串按 maxLength 实参中的数字截断,并加上省略号 ...。如果字符串比 maxLength 短,函数会返回原字符串。

在这个例子中,.displayInfo() 函数扩展了 User 接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface User {
    val name: String
    val email: String
}

fun User.displayInfo(): String = "User(name=$name, email=$email)"

// 继承并实现 User 接口的属性
class RegularUser(override val name: String, override val email: String) : User

fun main() {
    val user = RegularUser("Alice", "alice@example.com")
    println(user.displayInfo())
    // User(name=Alice, email=alice@example.com)
}

.displayInfo() 函数返回一个包含 RegularUser 实例的 name 和 email 的字符串。像这样在接口上定义扩展,适合你只想添加一次功能、就让所有实现该接口的类型都能使用的场景。

在这个例子中,.mostVoted() 函数扩展了 Map<String, Int> 类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fun Map<String, Int>.mostVoted(): String? {
    return maxByOrNull { (key, value) -> value }?.key
}

fun main() {
    val poll = mapOf(
        "Cats" to 37,
        "Dogs" to 58,
        "Birds" to 22
    )

    println("Top choice: ${poll.mostVoted()}")
    // Dogs
}

.mostVoted() 函数遍历调用它的映射中的键值对,并使用 maxByOrNull() 函数返回值最大的那个键值对的键。如果映射为空,maxByOrNull() 函数会返回 null。mostVoted() 函数使用安全调用 ?.,只在 maxByOrNull() 返回非空值时才访问 key 属性。

泛型扩展函数

要创建泛型扩展函数,请在函数名之前声明泛型类型参数,使其在接收者类型表达式中可用。在这个例子中,.endpoints() 函数扩展了 List<T>,其中 T 可以是任何类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fun <T> List<T>.endpoints(): Pair<T, T> {
    return first() to last()
}

fun main() {
    val cities = listOf("Paris", "London", "Berlin", "Prague")
    val temperatures = listOf(21.0, 19.5, 22.3)

    val cityEndpoints = cities.endpoints()
    val tempEndpoints = temperatures.endpoints()

    println("First and last cities: $cityEndpoints")
    // (Paris, Prague)
    println("First and last temperatures: $tempEndpoints")
    // (21.0, 22.3)
}

.endpoints() 函数返回一个包含列表首尾元素的对。在函数体内部,它调用 first() 和 last() 函数,并用 to 中缀函数把返回的值组合成一个 Pair。

关于泛型的更多信息,请参阅泛型函数。

可空接收者

你可以定义接收者类型为可空的扩展函数,这样即使变量的值为 null 也能调用它们。当接收者为 null 时,this 也是 null。请确保在函数中正确处理可空性。例如在函数体中使用 this == null 检查、安全调用 ?.,或 Elvis 运算符 ?:。

在这个例子中,你可以直接调用 .toString() 函数而不必检查 null,因为检查已经在扩展函数内部完成了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fun main() {
    // 可空 Any 上的扩展函数
    fun Any?.toString(): String {
        if (this == null) return "null"
        // 空检查之后,`this` 会被智能转换为非空 Any
        // 因此这次调用解析为普通的 toString() 函数
        return toString()
    }

    val number: Int? = 42
    val nothing: Any? = null

    println(number.toString())
    // 42
    println(nothing.toString())
    // null
}

扩展函数还是成员函数?

由于扩展函数和成员函数的调用记法相同,编译器如何知道该用哪一个?扩展函数是静态分发的,也就是说编译器在编译期根据接收者类型决定调用哪个函数。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fun main() {
    open class Shape
    class Rectangle: Shape()

    fun Shape.getName() = "Shape"
    fun Rectangle.getName() = "Rectangle"

    fun printClassName(shape: Shape) {
        println(shape.getName())
    }

    printClassName(Rectangle())
    // Shape
}

在这个例子中,编译器调用 Shape.getName() 扩展函数,因为参数 shape 声明的类型是 Shape。由于扩展函数是静态解析的,编译器根据声明的类型而非实际实例来选择函数。

所以,即使这个例子传入的是 Rectangle 实例,.getName() 函数仍会解析为 Shape.getName(),因为变量声明的类型是 Shape。

如果一个类有成员函数,并且存在接收者类型相同、名称相同、参数兼容的扩展函数,那么成员函数优先。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun main() {
    class Example {
        fun printFunctionType() { println("Member function") }
    }

    fun Example.printFunctionType() { println("Extension function") }

    Example().printFunctionType()
    // 成员函数
}

不过,扩展函数可以重载同名但签名不同的成员函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main() {
    class Example {
        fun printFunctionType() { println("Member function") }
    }

    // 同名但签名不同
    fun Example.printFunctionType(index: Int) { println("Extension function #$index") }

    Example().printFunctionType(1)
    // Extension function #1
}

在这个例子中,由于向 .printFunctionType() 函数传入了 Int,编译器会选择与该签名匹配的扩展函数。编译器会忽略不接受参数的成员函数。

匿名扩展函数

你可以定义不带名称的扩展函数。当你希望避免污染全局命名空间,或需要把某个扩展行为作为参数传递时,这会很有用。

例如,假设你想用一个一次性函数扩展数据类来计算运费,而又不想给它命名:

1
2
3
4
5
6
7
8
9
fun main() {
    data class Order(val weight: Double)
    val calculateShipping = fun Order.(rate: Double): Double = this.weight * rate

    val order = Order(2.5)
    val cost = order.calculateShipping(3.0)
    println("Shipping cost: $cost")
    // Shipping cost: 7.5
}

要把扩展行为作为参数传递,请使用带类型标注的 lambda 表达式。例如,假设你想在不定义具名函数的情况下判断某个数字是否落在某个区间内:

1
2
3
4
5
6
7
8
fun main() {
    val isInRange: Int.(min: Int, max: Int) -> Boolean = { min, max -> this in min..max }

    println(5.isInRange(1, 10))
    // true
    println(20.isInRange(1, 10))
    // false
}

在这个例子中,isInRange 变量保存的是类型为 Int.(min: Int, max: Int) -> Boolean 的函数。该类型是 Int 类上的扩展函数,接收 min 和 max 参数并返回 Boolean。

lambda 体 { min, max -> this in min..max } 检查调用该函数时所作用的 Int 值是否落在 min 和 max 参数之间的区间内。如果检查成功,lambda 返回 true。

更多信息请参阅 Lambda 表达式与匿名函数。

扩展属性

Kotlin 支持扩展属性,它们适合在不对所操作的类造成杂乱的情况下执行数据转换或创建 UI 显示辅助属性。

要创建扩展属性,请写出你想扩展的类名,后跟 . 以及属性名。

例如,假设你有一个表示用户的名和姓的数据类,并且想创建一个访问时返回电子邮件风格用户名的属性。你的代码可能像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
data class User(val firstName: String, val lastName: String)

// 用于获取用户名风格电子邮件句柄的扩展属性
val User.emailUsername: String
    get() = "${firstName.lowercase()}.${lastName.lowercase()}"

fun main() {
    val user = User("Mickey", "Mouse")
    // 调用扩展属性
    println("Generated email username: ${user.emailUsername}")
    // Generated email username: mickey.mouse
}

由于扩展并不会真正向类中添加成员,扩展属性没有高效的方式拥有幕后字段。这就是扩展属性不允许使用初始化器的原因。你只能通过显式提供 getter 和 setter 来定义它们的行为。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
data class House(val streetName: String)

// 无法编译,因为没有 getter 和 setter
// var House.number = 1
// 错误:扩展属性不允许使用初始化器

// 可以成功编译
val houseNumbers = mutableMapOf<House, Int>()
var House.number: Int
    get() = houseNumbers[this] ?: 1
    set(value) {
        println("Setting house number for ${this.streetName} to $value")
        houseNumbers[this] = value
    }

fun main() {
    val house = House("Maple Street")

    // 显示默认值
    println("Default number: ${house.number} ${house.streetName}")
    // Default number: 1 Maple Street

    house.number = 99
    // Setting house number for Maple Street to 99

    // 显示更新后的编号
    println("Updated number: ${house.number} ${house.streetName}")
    // Updated number: 99 Maple Street
}

在这个例子中,getter 使用 Elvis 运算符返回 houseNumbers 映射中的门牌号,若不存在则返回 1。要了解如何编写 getter 和 setter,请参阅自定义 getter 和 setter。

伴生对象扩展

如果一个类定义了伴生对象,你也可以为伴生对象定义扩展函数和扩展属性。与伴生对象的普通成员一样,可以只用类名作为限定符来调用它们。编译器默认把伴生对象命名为 Companion:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Logger {
    companion object { }
}

fun Logger.Companion.logStartupMessage() {
    println("Application started.")
}

fun main() {
    Logger.logStartupMessage()
    // Application started.
}

把扩展声明为成员

你可以在一个类内部为另一个类声明扩展。这类扩展有多个隐式接收者。隐式接收者是指其成员无需用 this 限定即可访问的对象:

  • 声明该扩展所在的类是分发接收者。
  • 扩展函数的接收者类型是扩展接收者。

来看这个例子,其中 Connection 类为 Host 类定义了一个名为 printConnectionString() 的扩展函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Host(val hostname: String) {
    fun printHostname() { print(hostname) }
}

class Connection(val host: Host, val port: Int) {
    fun printPort() { print(port) }

    // Host 是扩展接收者
    fun Host.printConnectionString() {
        // 调用 Host.printHostname()
        printHostname()
        print(":")
        // 调用 Connection.printPort()
        // Connection 是分发接收者
        printPort()
    }

    fun connect() {
        /*...*/
        // 调用扩展函数
        host.printConnectionString()
    }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    // kotl.in:443

    // 会触发错误,因为该扩展函数在 Connection 之外不可用
    // Host("kotl.in").printConnectionString()
    // 未解析的引用 'printConnectionString'。
}

这个例子在 Connection 类内部声明了 printConnectionString() 函数,因此 Connection 类是分发接收者。该扩展函数的接收者类型是 Host 类,因此 Host 类是扩展接收者。

如果分发接收者和扩展接收者有同名成员,扩展接收者的成员优先。要显式访问分发接收者,请使用带限定符的 this 语法:

1
2
3
4
5
6
7
8
class Connection {
    fun Host.getConnectionString() {
        // 调用 Host.toString()
        toString()
        // 调用 Connection.toString()
        this@Connection.toString()
    }
}

重写成员扩展

你可以把成员扩展声明为 open 并在子类中重写它们,这在你想为每个子类定制扩展行为时很有用。编译器对每种接收者类型的处理方式不同:

| 接收者类型 | 解析时机 | 分发方式 |

| 分发接收者 | 运行时 | 虚分发 | | 扩展接收者 | 编译期 | 静态 |

来看这个例子,其中 User 类是 open 的,Admin 类继承自它。NotificationSender 类为 User 和 Admin 类都定义了 sendNotification() 扩展函数,而 SpecialNotificationSender 类重写了它们:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
open class User

class Admin : User()

open class NotificationSender {
    open fun User.sendNotification() {
        println("Sending user notification from normal sender")
    }

    open fun Admin.sendNotification() {
        println("Sending admin notification from normal sender")
    }

    fun notify(user: User) {
        user.sendNotification()
    }
}

class SpecialNotificationSender : NotificationSender() {
    override fun User.sendNotification() {
        println("Sending user notification from special sender")
    }

    override fun Admin.sendNotification() {
        println("Sending admin notification from special sender")
    }
}

fun main() {
    // 分发接收者是 NotificationSender
    // 扩展接收者是 User
    // 解析为 NotificationSender 中的 User.sendNotification()
    NotificationSender().notify(User())
    // Sending user notification from normal sender

    // 分发接收者是 SpecialNotificationSender
    // 扩展接收者是 User
    // 解析为 SpecialNotificationSender 中的 User.sendNotification()
    SpecialNotificationSender().notify(User())
    // Sending user notification from special sender

    // 分发接收者是 SpecialNotificationSender
    // 扩展接收者是 User,而不是 Admin
    // notify() 函数把 user 声明为 User 类型
    // 因此静态解析为 SpecialNotificationSender 中的 User.sendNotification()
    SpecialNotificationSender().notify(Admin())
    // Sending user notification from special sender
}

分发接收者在运行时通过虚分发解析,这使得 main() 函数中的行为更容易理解。可能让你意外的是,当你在 Admin 实例上调用 notify() 函数时,编译器根据声明的类型 user: User 来选择扩展,因为它是静态解析扩展接收者的。

扩展与可见性修饰符

扩展使用的可见性修饰符与在同一作用域中声明的普通函数相同,包括声明为其他类成员的扩展。

例如,在文件顶层声明的扩展可以访问同一文件中其他 private 顶层声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 文件:StringUtils.kt

private fun removeWhitespace(input: String): String {
    return input.replace("\\s".toRegex(), "")
}

fun String.cleaned(): String {
    return removeWhitespace(this)
}

fun main() {
    val rawEmail = "  user @example. com  "
    val cleaned = rawEmail.cleaned()
    println("Raw:     '$rawEmail'")
    // Raw:     '  user @example. com  '
    println("Cleaned: '$cleaned'")
    // Cleaned: 'user@example.com'
    println("Looks like an email: ${cleaned.contains("@") && cleaned.contains(".")}")
    // Looks like an email: true
}

而如果扩展声明在接收者类型之外,它就无法访问接收者的 private 或 protected 成员:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class User(private val password: String) {
    fun isLoggedIn(): Boolean = true
    fun passwordLength(): Int = password.length
}

// 在类外部声明的扩展
fun User.isSecure(): Boolean {
    // 无法访问 password,因为它是 private 的:
    // return password.length >= 8

    // 改为依赖公开成员:
    return passwordLength() >= 8 && isLoggedIn()
}

fun main() {
    val user = User("supersecret")
    println("Is user secure: ${user.isSecure()}")
    // Is user secure: true
}

如果扩展被标记为 internal,则只能在其所在的模块中访问:

1
2
3
4
5
// 网络模块
// JsonParser.kt
internal fun String.parseJson(): Map<String, Any> {
    return mapOf("fakeKey" to "fakeValue")
}

扩展的作用域

大多数情况下,你会把扩展定义在顶层,直接放在包下:

1
2
3
package org.example.declarations

fun List<String>.getLongestString() { /*...*/}

要在声明它的包之外使用扩展,请在调用处导入它:

1
2
3
4
5
6
7
8
package org.example.usage

import org.example.declarations.getLongestString

fun main() {
    val list = listOf("red", "green", "blue")
    list.getLongestString()
}

更多信息请参阅导入。