Kotlin 官方学习教程之扩展

x33g5p2x  于2022-03-08 转载在 其他  
字(3.4k)|赞(0)|评价(0)|浏览(523)

扩展

类似于 C# 和 Gosu, Kotlin 也提供了一种可以在不继承父类也不使用类似装饰器这样的设计模式的情况下对指定类进行扩展的功能。这是通过称为扩展名的特殊声明来实现的。 Kotlin 支持函数扩展和属性扩展。

函数扩展

要声明一个函数扩展,我们需要在函数前加一个接收者类型作为前缀。下面我们会为 MutableList 添加一个 swap 函数:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' 对应 list
    this[index1] = this[index2]
    this[index2] = tmp
}

扩展函数中的 this 关键字对应接收者对象。现在我们可以在任意一个 MutableList 实例中调用 swap 对象。

val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // 在 'swap()' 中 'this' 将会持有值 'l'

当然,这个函数对于任何 MutableList 都是有意义的,我们可以将它通用:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' 对应 list
    this[index1] = this[index2]
    this[index2] = tmp
}

我们在函数名称之前声明通用类型属性,使之可以接收任何类型的参数。

扩展是被静态解析的

扩展实际上并没有修改它所扩展的类。定义一个扩展,你并没有在类中插入一个新的成员,只是让这个类的实例对象能够通过.调用新的函数。

需要强调的是扩展函数是静态分发的,举个例子,它们并不是接受者类型的虚拟方法。这意味着扩展函数的调用时由发起函数调用的表达式的类型决定的,而不是在运行时动态获得的表达式的类型决定。例如:

open class C

class D: C()

fun C.foo() = "c"

fun D.foo() = "d"

fun printFoo(c: C) {
    println(c.foo())
}

printFoo(D())

这个例子会输出 c,因为这里扩展函数的调用决定于声明的参数 c 的类型,也就是 C。

如果有同名同参数的成员函数和扩展函数,调用的时候必然会使用成员函数,比如:

class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

如果我们对 C 的实例 c 调用 c.foo,会输出 “member”, 而不是”extension”。

但是你可以用不同的函数签名通过扩展函数的方式重载函数的成员函数,比如下面这样:

class C {
    fun foo() { println("member") }
}

fun C.foo(i: Int) { println("extension") }

对 C().foo(1) 的调用会输出 “extension”。

可空接收者

请注意扩展可以定义一个可空的接收类型。这样的扩展使得即使是一个空对象也可以调用它,然后在扩展内部进行 this == null 的检查。这样那就可以在 Kotlin 的任意调用 toString() 而无需进行空指针检查,空指针检查延后到扩展中进行。

fun Any?.toString(): String {
    if (this == null) return "null"
    // 在空指针检查后, 'this' 被自动转为非空类型,因此 toString() 可以被解析到任何类的成员函数中
    return toString()
}

属性扩展

和函数相似,Kotlin 支持属性扩展:

val <T> List<T>.lastIndex: Int
    get() = size - 1

请注意,由于扩展并不会真正的往类中添加成员,因此也没有办法让扩展属性拥有备用字段。这也是为什么初始化函数不允许有扩展属性的原因。扩展属性只能通过明确提供 getter / setter 来定义。

例子:

val Foo.bar = 1 // 错误: 初始化函数不允许有扩展属性

伴随对象扩展

如果一个对象定义了一个伴随对象,你同样可以给伴随对象定义扩展函数和扩展属性。

class MyClass {
    companion object { }  // 被称作 "Companion"
}

fun MyClass.Companion.foo() {
    // ...
}

和普通伴随对象的成员一样,他们可以只通过类名就调用:

MyClass.foo()

扩展的域

大多数时候我们在 top level 中定义,例如直接早包名下定义:

package foo.bar

fun Baz.goo() { ... }

要在声明的包外使用这样一个扩展,我们需要在使用它的地方导入它:

package com.example.usage

import foo.bar.goo // 导入所有叫做 "goo" 的扩展
                   // 或者
import foo.bar.*   // 导入 "foo.bar" 包下所有内容

fun usage(baz: Baz) {
    baz.goo()
}

声明扩展成员

在一个类中,你可以为另一个类声明一个扩展。在这样一个扩展中,有多个隐式接收器 - 对象成员可以在没有限定符的情况下被访问。声明扩展名的类的实例被称为调度接收方,声明扩展方法的接收方类型的实例称为扩展接收方。

class D {
    fun bar() { ... }
}

class C {
    fun baz() { ... }

    fun D.foo() {
        bar()   // 调用 D.bar
        baz()   // 调用 C.baz
    }

    fun caller(d: D) {
        d.foo()   // 调用扩展函数
    }
}

在发送接收机的成员与分机接收者之间发生名称冲突的情况下,分机接收者优先。要引用发送接收器的成员,您可以使用此语法。

class C {
    fun D.foo() {
        toString()         // 调用D.toString()
        this@C.toString()  // 调用 C.toString()
    }

声明为成员的扩展可以在子类中声明为 open 型的并且可以被重写。这意味着这种函数的调度对于调度接收器类型是虚拟的,但是关于扩展接收器类型是静态的。

open class D {
}

class D1 : D() {
}

open class C {
    open fun D.foo() {
        println("D.foo in C")
    }

    open fun D1.foo() {
        println("D1.foo in C")
    }

    fun caller(d: D) {
        d.foo()   // 调用扩展函数
    }
}

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    }

    override fun D1.foo() {
        println("D1.foo in C1")
    }
}

C().caller(D())   // 输出 "D.foo in C"
C1().caller(D())  // 输出 "D.foo in C1" - 调度接收器被虚拟地解析
C().caller(D1())  // 输出 "D.foo in C" - 扩展接收机静态解析

动机

在 Java 中我们经常使用一系列名为 “*Utils”的类,如 FileUtils,StringUtils 等等。著名的 java.util.Collections 也是其中的一员。但令人不快的是我们必须像下面这样使用它们:

// Java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list))

由于这些类名总是不变的,我们可以使用静态导入并这样使用:

// Java
swap(list, binarySearch(list, max(otherList)), max(list))

这样就好多了,但是这样我们只能从 IDE 自动完成代码获得很少甚至得不到帮助,如果我们能像下面这样就好了:

// Java
list.swap(list.binarySearch(otherList.max()), list.max())

但是我们不想在 List 内实现所有可能的方法,对吗?这是扩展帮助我们的地方。

相关文章