Kotlin学习之类与对象篇5—扩展(Extensions)

Kotlin与C#和Gosu类似,都提供让类扩展新功能的能力,并且不用继承类或使用设计模式,比如装饰者模式。该功能通过扩展(extensions)来实现。Kotlin支持扩展方法(extension functions)和扩展属性(extension properties)。

扩展方法

要声明一个扩展方法,需要在其名称前添加一个接收者类型,也就是被扩展的类型。

下面的代码给MutableList<Int>添加一个swap方法:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' 指当前List
    this[index1] = this[index2]
    this[index2] = tmp
}

扩展方法内的this关键字对应于接收者对象(调用扩展方法时位于点.之前的对象)。现在,我们可以对任意MutableList<Int>类型的对象调用此方法。

val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // `swap()` 方法内的 `this` 持有 `l` 的值

当然,这个方法对于任意类型的MutableList<T>都可用,因此我们可以让它变成通用方法:

fun <T> MutableList<T>.swap(index1: Int, index2: Int){
    val temp = this[index1]
    this[index1] = this[index2]
    this[index2] = temp
}

我们在方法名称前面声明泛型参数,以让它在接收者类型表达式中可用。

扩展是静态解析的

扩展实际上没有改变它们扩展的类。通过定义一个扩展,你并没有给类添加新的成员,而仅仅是让该类型的变量可以通过点符号.调用新的方法。

我们想强调的是,扩展方法是静态分发的,即他们不是接收者类型的虚方法。这意味着被调用的扩展方法由调用方法的表达式的类型决定,而不是由在运行时计算该表达式的结果的类型决定。

例如:

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, 所以调用的是类C的扩展方法。

如果一个类有一个成员方法,以及一个扩展方法,并且这两个方法拥有相同的接收者类型,相同的名称,而且接受相同的参数,这种情况下总是取成员方法。例如:

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

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

如果我们调用C类型任何c的c.foo(),它都会打印member,而不是extension

然而,只要这两个方法接收的参数不同,就完全OK。

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

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

调用C().foo(1)将打印extension

可空的接收者

注意扩展方法可以用可空接收者来定义。即使对象变量的值为null,也可以调用此类扩展。在扩展方法体中可以使用this == null 来检测。这让你以在Kotlin中可以调用toString()方法而不用进行空值检测:检测发生在扩展方法内部。

fun Any?.toString(): String {
    if (this == null) return "null"
    // null检测之后,“this”会自动转换为非空类型,所以下面的 toString() 解析为 Any 类的成员函数
    // 
    return toString()
}

扩展属性

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

由于扩展实际上没有向类中插入成员,因此扩展属性是没有backing field的。因此扩展属性不允许有初始化器。它们的行为只能通过提供明确的gettters/setters来定义。

例子:

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

伴生对象(Companion Object)的扩展

伴生对象的详细定义将在后面的章节给出。

如果一个类定义了伴生对象,也可以给伴生对象定义扩展方法和扩展属性:

class MyClass {
    companion object { }  // 伴生对象
}

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

和伴生对象的常规成员一样,它们只需要使用类名作为限定符来调用。

MyClass.foo()   //调用MyClass类伴生对象的扩展方法foo()

扩展的作用域

多数情况下我们在顶层中定义扩展,即直接在包下。

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()
}

扩展声明为成员

在一个类中,你可以为另一个类声明扩展。在这样的扩展内,有许多隐式接收者(implicit receiver),其对象成员的访问不需要使用限定符。声明扩展所在的类的实例被称作分发接收者(dispatch receiver), 扩展方法的接收者类型的实例被称作扩展接收者(extension receiver)。

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()  // 指明接收者类型    
    }

声明为类成员的扩展能声明为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"

可见性说明

扩展的可见性与相同作用域内声明的其他实体的可见性相同。例如:
- 同一文件中,在顶层声明的扩展可以访问其它private的顶层声明。
- 如果一个扩展声明在其接收者类型之外,这样的扩展不能访问其接收者的private成员。

动机

在Java中,我们将类命名为"*Utils"FileUtilsStringUtils 等,著名的 java.util.Collections 也属于同一种命名方式。 关于这些 Utils-类的不愉快的部分是代码写成这样:

// 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 类内实现所有可能的方法,对吧? 这时候扩展将会帮助我们。

猜你喜欢

转载自blog.csdn.net/chenrenxiang/article/details/80986292