💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
扩展是Kotlin实现特设多态的一种非常重要的语言特性。在本节中,我们将继续探讨这种技术。 [TOC] ### 扩展与开放封闭原则 对开发者而言,业务需求总是在不断变动的。熟悉设计模式的读者应该知道,在修改现有代码的时候,我们应该遵循开放封闭原则,即:**软件实体应该是可扩展,而不可修改的**。也就是说,对扩展开放,而对修改是封闭的。 ***** **开放封闭原则概念** **开放封闭原则(OCP, Open Closed Principle)是所有面向对象原则的核心**。软件设计本身所追求的目标就是封装变化、降低耦合,而开放封闭原则正是对这一目标的最直接体现。其他的设计原则,很多时候是为实现这一目标服务的,例如以替换原则实现最佳的、正确的继承层次,就能保证不会违反开放封闭原则。 ***** 实际情况并不乐观,比如在进行Android开发的时候,为了实现某个需求,我们引入了一个第三方库。但某一天需求发生了变动,当前库无法满足,且库的作者暂时没有升级的计划。这时候也许你就会开始尝试对库源码进行修改。这就违背了开放封闭原则。随着需求的不断变更,问题可能就会如滚雪球般增长。 Java中一种惯常的应对方案是让第三方库类继承一个子类,然后添加新功能。然而,正如我们谈论过的那样,强行的继承可能违背“里氏替换原则”。 **更合理的方案是依靠扩展这个语言特性。Kotlin通过扩展一个类的新功能而无须继承该类,在大多数情况下都是一种更好的选择,从而我们可以合理地遵循软件设计原则**。 ### 使用扩展函数、属性 扩展函数的声明非常简单,它的关键字是`<Type>`。此外我们需要一个“接收者类型(recievier type)”(通常是类或接口的名称,也就是谁可以调用这个函数)来作为它的前缀。 以`MutableList<Int>`为例,我们为其扩展一个exchange方法,代码如下: ``` fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) { val tmp = this[fromIndex] this[fromIndex] = this[toIndex] this[toIndex] = tmp } fun main(args: Array<String>) { val list = mutableListOf(1, 2, 3) list.exchange(1, 2) } ``` `MutableList<T>`是Kotlin标准库Collections中的List容器类,这里作为接收者recievier type,exchange是扩展函数名。其余和Kotlin声明一个普通函数并无区别。Kotlin的this要比Java更灵活,**这里扩展函数体里的this代表的是接收者类型的对象**。 这里需要注意的是:**Kotlin严格区分了接收者是否可空。如果你的函数是可空的,你需要重写一个可空类型的扩展函数**。 我们可以非常方便地对该函数进行调用,代码如下: ``` val list = mutableListOf(1,2,3) list.exchange(1,2) ``` #### 1.扩展函数的实现机制 扩展函数的使用如此方便,会不会对性能造成影响呢?为了解决这个疑惑,我们有必要对Kotlin扩展函数的实现进行探究。我们以之前的`MutableList<Int>.exchange`为例,它对应的Java代码如下: ``` import java.util.List; import kotlin.Metadata; import kotlin.jvm.internal.Intrinsics; import org.jetbrains.annotations.NotNull; @Metadata( mv = {1, 1, 1}, bv = {1, 0, 0}, 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", "production sources for module FPKotlin"} ) public final class ExSampleKt { public static final void exchange(@NotNull List $receiver, int fromIndex, int toIndex) { Intrinsics.checkParameterIsNotNull($receiver, "$receiver"); int tmp = ((Number)$receiver.get(fromIndex)).intValue(); $receiver.set(fromIndex, $receiver.get(toIndex)); $receiver.set(toIndex, Integer.valueOf(tmp)); } } ``` 结合以上Java代码可以看出,我们**可以将扩展函数近似理解为静态方法。而熟悉Java的读者应该知道静态方法的特点:它独立于该类的任何对象,且不依赖类的特定实例,被该类的所有实例共享。此外,被public修饰的静态方法本质上也就是全局方法**。 综上所述,我们可以得出结论:**扩展函数不会带来额外的性能消耗**。 #### 2.扩展函数的作用域 既然扩展函数不会带来额外的性能消耗,那我们就可以放心地使用它。它的作用域范围是怎么样的呢?**一般来说,我们习惯将扩展函数直接定义在包内**,例如之前的exchange例子,我们可以将其放在com.example.extension包下: ``` package com.example.extension fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) { val tmp = this[fromIndex] this[fromIndex] = this[toIndex] this[toIndex] = tmp } ``` 我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理。 ``` class Extends { fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) { val tmp = this[fromIndex] this[fromIndex] = this[toIndex] this[toIndex] = tmp } } ``` 当扩展函数定义在Extends类内部时,情况就与之前不一样了:这个时候你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)。你可能会猜想,是不是它被声明为private方法了?那我们尝试在exchange方法前加上public关键字: ``` public fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) { … } ``` 结果不尽如人意,此时我们依旧无法调用到(实际上Kotlin中成员方法默认就是用public修饰的)。是什么原因呢?借助IDEA我们可以查看到它对应的Java代码,这里展示关键部分: ``` public static final class Extends { public final void exchange(@NotNull List $receiver, int fromIndex, int toIndex) { Intrinsics.checkParameterIsNotNull($receiver, "$receiver"); int tmp = ((Number)$receiver.get(fromIndex)).intValue(); $receiver.set(fromIndex, $receiver.get(toIndex)); $receiver.set(toIndex, Integer.valueOf(tmp)); } } ``` 我们看到,exchange方法上已经没有static关键字的修饰了。所以**当扩展方法在一个Class内部时,我们只能在该类和该类的子类中进行调用**。此外你可能还会想到:如果我用private修饰这个扩展函数,又会有什么结果?这个问题留给读者自行探究。 #### 扩展属性 与扩展函数类似,我们还能为一个类添加扩展属性。比如我们想给`MutableList<Int>`添加判断一个判断和是否为偶数的属性sumIsEven: ``` val MutableList<Int>.sumIsEven: Boolean get() = this.sum() % 2 == 0 ``` 这样就可以像调用扩展函数一样调用它了: ``` val list = mutableListOf(2,2,4) list.sumIsEven ``` 但是,如果你准备给这个属性添加上默认值,并且写出如下代码: // 编译错误:扩展属性不能有初始化器 ``` val MutableList<Int>.sumIsEven: Boolean = false get() = this.sum() % 2 == 0 ``` 以上代码编译不能通过,这是为什么呢? 其实,与扩展函数一样,其本质也是对应Java中的静态方法(我们反编译成Java代码后可以看到一个getSumIsEven的静态方法,与扩展函数类似)。**由于扩展没有实际地将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器的原因。它们的行为只能由显式提供的getters和setters定义**。 ***** **幕后字段** 在Kotlin中,如果属性中存在访问器使用默认实现,那么Kotlin会自动提供幕后字段filed,其仅可用于自定义getter和setter中。 ***** ### 扩展的特殊情况 前面,我们对Kotlin的扩展函数已经有了基本的认识,相信大部分读者已经被扩展函数所吸引,并且已经想好如何利用扩展函数进行实战。但在此之前,还是让我们先看一些扩展中特殊的情况,或者说是扩展的局限之处。 #### 1.类似Java的静态扩展函数 在Kotlin中,如果你需要声明一个静态的扩展函数,开发者**必须将其定义在伴生对象(companion object)上**。所以我们需要这样定义带有伴生对象的类: ``` class Son { companion object { val age = 10 } } ``` Son类中已经有一个伴生对象,如果我们现在**不想在Son中定义扩展函数,而是在Son的伴生对象上定义**,可以这么写: ``` fun Son.Companion.foo() { println("age = $age") } ``` 这样,**我们就能在Son没有实例对象的情况下,也能调用到这个扩展函数,语法类似于Java的静态方法**。 ``` object Test { @JvmStatic fun main(args: Array<String>) { Son.foo() } } ``` 一切看起来都很顺利,但是当我们想让第三方类库也支持这样的写法时,我们发现,并**不是所有的第三方类库中的类都存在伴生对象,我们只能通过它的实例来进行调用,但这样会造成很多不必要的麻烦**。 #### 2.成员方法优先级总高于扩展函数 已知我们有如下类: ``` class Son { fun foo() = println("son called member foo") } ``` 它包含一个成员方法foo(),假如我们哪天心血来潮,想对这个方法做特殊实现,利用扩展函数可能会写出如下代码: ``` fun Son.foo() = println("son called extention foo") object Test { @JvmStatic fun main(args: Array<String>) { Son().foo() } } ``` 在我们的预期中,我们希望调用的是扩展函数foo(),但是输出结果为: son called member foo。这表明:**当扩展函数和现有类的成员方法同时存在时,Kotlin将会默认使用类的成员方法**。看起来似乎不够合理,并且很容易引发一些问题:我定义了新的方法,为什么还是调用到了旧的方法? 但是换一个角度思考,**在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成混淆。对于第三方类库来说甚至是一场灾难:我们把不应该更改的方法改变了**。所以在使用时,我们必须注意:**同名的类成员方法的优先级总高于扩展函数**。 #### 3.类的实例与接收者的实例 前面的例子中提到过,我们发现Kotlin中的this比在Java中更灵活。以扩展函数为例,**当在扩展函数里调用this时,指代的是接收者类型的实例**。那么如果这个扩展函数声明在一个object内部,我们如何通过this获取到类的实例呢?参考如下代码: ``` class Son{ fun foo(){ println("foo in Class Son") } } object Parent { fun foo() { println("foo in Class Parent") } @JvmStatic fun main(args: Array<String>) { fun Son.foo2() { this.foo() this@Parent.foo() } Son().foo2() } } ``` 这里我们**可以用this@类名来强行指定调用的this**。另外值得一提的是:**如果Son扩展函数在Parent类内,我们将无法对其调用**。 ``` class Son{ fun foo(){ println("foo in Class Son") } } class Parent { fun foo() { println("foo in Class Parent") } fun Son.foo2() { this.foo() this@Parent.foo() } } object Test { @JvmStatic fun main(args: Array<String>) { Son().foo2() } } ``` 这是为什么呢?来看看Parent对应的Java代码,以下为核心部分: ``` public final class Parent { public final void foo() { String var1 = "foo in Class Parent"; System.out.println(var1); } public final void foo2(@NotNull Son $receiver) { Intrinsics.checkParameterIsNotNull($receiver, "$receiver"); $receiver.foo(); this.foo(); } } ``` 即使我们设置访问权限为public,它也只能在该类或者该类的子类中被访问,如果我们设置访问权限为private,那么在子类中也不能访问这个扩展函数。 ### 标准库中的扩展函数:run、let、also、takeIf Kotlin标准库中有一些非常实用的扩展函数,除了之前我们接触过的apply、with函数之外,我们再来了解下let、run、also、takeIf。 #### 1. run 先来看下run方法,它是利用扩展实现的,定义如下: ``` public inline fun <T, R> T.run(block: T.() -> R): R = block() ``` 简单来说,run是任何类型T的通用扩展函数,run中执行了返回类型为R的扩展函数block,最终返回该扩展函数的结果。 在run函数中我们拥有一个单独的作用域,能够重新定义一个nickName变量,并且它的作用域只存在于run函数中。 ``` fun testFoo() { val nickName = "Prefert" run { val nickName = "YarenTang" println(nickName) // YarenTang } println(nickName) // Prefert } ``` 这个范围函数本身似乎不是很有用。但是相比范围,还有一点不错的是,它返回范围内最后一个对象。 例如现在有这么一个场景:用户点击领取新人奖励的按钮,如果用户此时没有登录则弹出loginDialog,如果已经登录则弹出领取奖励的getNewAccountDialog。我们可以使用以下代码来处理这个逻辑: ``` run { if (! islogin) loginDialog else getNewAccountDialog }.show() ``` #### 2. let 在介绍可空类型的时候,我们接触了let方法,来看看它的定义: ``` public inline fun <T, R> T.let(block: (T) -> R): R = block(this) ``` **let和apply类似,唯一不同的是返回值:apply返回的是原来的对象,而let返回的是闭包里面的值**。细心的读者应该察觉到,我们在介绍可空类型的时候,大量使用了let语法,简单回顾一下: ``` data class Student(age: Int) class Kot { val student: Student? = getStu() fun dealStu() { val result = student? .let { println(it.age) it.age } } } ``` **由于let函数返回的是闭包的最后一行,当student不为null的时候,才会打印并返回它的年龄**。与run一样,它**同样限制了变量的作用域**。 #### 3. also also是Kotlin 1.1版本中新加入的内容,它**像是let和apply函数的加强版**。 ``` public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this } ``` 与apply一致,它的返回值是该函数的接收者: ``` class Kot { val student: Student? = getStu() var age = 0 fun dealStu() { val result = student? .also { stu -> this.age += stu.age println(this.age) println(stu.age) this.age } } } ``` 我将它的隐式参数指定为stu,假设student?不为空,我们会发现返回了student,并且总年龄age增加了。 值得注意的是:**如果使用apply,由于它内部是一个扩展函数,this将指向stu而不是Kot,此处我们将无法调用到Kot下的age**。 #### 4. takeIf 如果我们**不仅仅只想判空,还想加入条件**,这时let可能显得有点不足。让我们来看看takeIf。 ``` public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null ``` 这个函数也是在Kotlin1.1中新增的。当接收器满足某些条件时它才会执行。如果我们想对成年的学生操作,可以这样写: ``` val result = student.takeIf { it.age >= 18 }.let { ... } ``` 我们发现,这**与集合中的filter异曲同工,不过takeIf只操作单条数据**。与takeIf相反的还有takeUnless,即接收器不满足特定条件才会执行。 除了这些函数外,Kotlin标准库中还有很多方便的扩展函数。