0%

第7章:多态和扩展

多态的不同方式

当一个子类继承父类的时候,这就是子类型多态;另一种熟悉的多态是参数多态,泛型就是参数多态常见的形式。

对第三方类扩展

考虑业务类 ClassA、ClassB是第三方引入的并且不能修改,如果我们想要给它们扩展一些方法,比如将对象转换为 Json 字符串,那么利用以前多态技术就会显得麻烦。幸运的是,Kotlin 支持扩展语法:

1
2
3
fun ClassA.toJson(): String = {
...
}

就可以很方便地添加了toJson方法,需要注意的是,扩展属性和方法的实现运行在ClassA的实例,他们的定义操作不会修改 ClassA 类本身,所以被扩展的第三方类免于被污染。

特设多态与运算符重载

除了子类型多态、参数多态外,还有一种灵活的多态——特设多态。可能概念不好理解,举个具体的例子,你想定义一个通用的sum方法,也许会这么写:

1
fun <T> sum(x: T, y: T): T = x + y

但编译器会报错,因为某些类型不一定支持加法操作。这时候,我们希望定义一种通用的“加法语义上的操作”,可以定义一个通用的 Summable 接口,然后让需要支持假发操作的类来实现它:

1
2
3
4
5
6
7
interface Sumable<T> {
fun plusThat(that: T): T
}

data class Len(val v: Int): Sumable<Len> {
override fun plusThat(that: Len) = Len(this.v + that.v)
}

可以发现,这并没有什么问题。然而,如果要针对不可修改的第三方类扩展加法操作时,这种方式也会遇到问题。于是,又想到了Kotlin的扩展,针对以上例子,我们完全可以采用扩展的语法来解决问题,此外,Kotlin 原生支持运算符重载可以很好解决上述问题:

1
2
3
4
5
6
7
8
9
data class Area(val value: Double)

operator fun Area.plus(that: Area): Area {
return Area(this.value + that.value)
}

fun main(args: Array<String>) {
println(Area(1.0) + Area(2.0)) //运行结果: Area(value=3.0)
}

通过 operator 关键字以及Kotlin中内置可重载的运算符plus,就实现了功能。operator 的作用是,将一个函数标记为重载一个操作符或者实现一个约定,这里的plus是Kotlin规定的函数名。除了plus,我们还可以通过重载减法(minus)、乘法(times)、触发(div)、取余(mod,在kotlin 1.1 版本开始被 rem 替代)。此外,回忆kotlin中常用语法,也是用这种神奇的语言特性实现的,比如:

1
a in list// 转换为 list.contains(a)

扩展: 为别的类添加方法、属性

继续深入Kotlin 的特设多态语言特性

扩展与开放封闭原则

熟悉设计模式的读者知道,在修改现有代码时,我们应该遵循开放封闭原则,对扩展开放,对修改封闭。然而实际并不乐观,比如Android开发,为实现某个需求,引入了第三方库,但是需求发生变动后,当前库无法满足需求,且库的作者没有升级计划。这时候你也许就会考虑对源码修改,这就违背了开放封闭原则。

Java中一种惯常做法是继承第三方类,添加新功能,但是,强行的继承可能违背“里氏替换原则”。更合理的方案,就是通过Kotlin的扩展功能。

使用扩展函数、属性

扩展函数的声明的关键字是 。此外,我们需要一个“接收者类型”(通常是类或者接口)来作为它的前缀,以为 MutableList扩展exchange方法为例:

1
2
3
4
5
fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
val temp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = temp
}

MutableList 是Kotlin标准库中的类,这里作为接收者类型,exchange是扩展函数名。Kotlin的this要比Java的更灵活,这里扩展函数体内的this代表的是接收者类型的对象。注意,Kotlin中是严格区分接收者是否可空的,如果你的函数是可空的,你需要重写一个可空类型的扩展函数

扩展函数的实现机制

扩展函数这么方便,会不会对性能造成影响呢?以 MutableList.exchange 为例,对应的Java代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Metadata(
mv = {1, 5, 1},
k = 2,
d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"},
d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "CommoKotlin"}
)

public final class KotClassKt {
public static final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
Intrinsics.checkNotNullParameter($this$exchange, "$this$exchange");
int temp = ((Number)$this$exchange.get(fromIndex)).intValue();
$this$exchange.set(fromIndex, $this$exchange.get(toIndex));
$this$exchange.set(toIndex, temp);
}
}

结合上述Java代码可以看出,我们可以将扩展函数近似理解为静态方法。我们知道静态方法的特点:不依赖类的特定实例,被该类所有的实例共享,并且,用public修饰,本质上也就是个全局方法,所以,扩展函数不会带来额外的性能消耗

扩展函数的作用域

一般来说,我们习惯将扩展函数直接定义在包内,例如之前的 exchange 例子,我们可以将其放在 com.example.extension包下:

1
2
3
4
5
package com.example.extension

fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
...
}

我们知道,在同一个包内是可以直接调用exchange方法的,如果需要在其他包中调用,只需要import即可,这与Java全局静态方法类似。与此同时,在实际开发中,我们可能会将扩展函数定义在一个 Class 内部统一管理:

1
2
3
4
5
6
7
class Extends {
fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
val temp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = temp
}
}

但你会发现,之前的exchange方法无法调用了!我们看下它的Java源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Metadata(
mv = {1, 5, 1},
k = 1,
d1 = {"\u0000\u001c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J \u0010\u0003\u001a\u00020\u0004*\b\u0012\u0004\u0012\u00020\u00060\u00052\u0006\u0010\u0007\u001a\u00020\u00062\u0006\u0010\b\u001a\u00020\u0006¨\u0006\t"},
d2 = {"Lcom/glassx/Extends;", "", "()V", "exchange", "", "", "", "fromIndex", "toIndex", "CommoKotlin"}
)
public final class Extends {
public final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
Intrinsics.checkNotNullParameter($this$exchange, "$this$exchange");
int temp = ((Number)$this$exchange.get(fromIndex)).intValue();
$this$exchange.set(fromIndex, $this$exchange.get(toIndex));
$this$exchange.set(toIndex, temp);
}
}

我们才发现exchange方法没有static关键字了,所以,当扩展方法在一个 Class 内部时,我们只能在该类和该类的子类中调用

扩展属性

与扩展函数一样,我们还能为一个类添加扩展属性,比如为MutableList添加一个判断和是否为偶数的属性:

1
2
3
4
5
6
val MutableList<Int>.sumIsEven: Boolean
get() = this.sum() % 2 == 0

//使用
val list = mutableListOf(2,2,4)
list.sumIsEven

但是,如果你准备给这个属性添加默认值,并且写出如下代码:

1
2
val MutableList<Int>.sumIsEven: Boolean = false
get() = this.sum() % 2 == 0

代码会编译不通过,告知扩展属性不能有初始化器。这是为什么呢?其实,与扩展函数一样,其本质也是对应Java中的静态方法,反编译成Java后,会看到一个 getSumIsEven 的静态方法:

1
2
3
4
5
6
7
8
9
10
11
12
@Metadata(
mv = {1, 5, 1},
k = 2,
d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u000b\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\"\u001b\u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00028F¢\u0006\u0006\u001a\u0004\b\u0004\u0010\u0005¨\u0006\u0006"},
d2 = {"sumIsEven", "", "", "", "getSumIsEven", "(Ljava/util/List;)Z", "CommoKotlin"}
)
public final class ExtendsKt {
public static final boolean getSumIsEven(@NotNull List $this$sumIsEven) {
Intrinsics.checkNotNullParameter($this$sumIsEven, "$this$sumIsEven");
return CollectionsKt.sumOfInt((Iterable)$this$sumIsEven) % 2 == 0;
}
}

由于扩展没有实际地将成员插入类中,因此对扩展属性来说幕后字段是无效的,它们的行为只能由显式提供的 getters 和setters 定义

幕后字段:如果属性中存在访问器使用默认实现,那么Kotlin 会自动提供幕后字段 field ,其仅可用于 getter 和 setter 中。

扩展的特殊情况

类似Java的静态扩展函数

在Kotlin中,如果要声明一个静态的扩展函数,必须要有 伴生对象(companion object)上,所以我们要这样定义带有伴生对象的类:

1
2
3
4
5
class Son {
companion object {
val age = 10
}
}

在已有伴生对象的情况下,如果不想再Son中定义扩展函数,而是在Son的伴生对象上定义,可以这么写:

1
2
3
4
5
6
7
8
fun Son.Companion.foo() {
println("age = $age")
}

//这样在没有Son的实例对象的情况下,也能调用,使用:
fun main(args: Array<String>) {
Son.foo()
}

成员方法优先级总是高于扩展函数

如果扩展函数和现有类的成员方法一样,那么优先调用成员方法,这一点好理解,我们不应该更改原有实现。

类的实例与接收者实例

略,没看清楚表达什么

标准库中的扩展函数:run、let、also、takeIf

Android中扩展的应用

扩展不是万能的

谢谢你的鼓励