Kotlin的扩展(Extension),主要分为两种语法:
1)扩展函数
2)扩展属性
从语法上看,扩展看起来就像是我们从类的外部为它扩展了新的成员。
这在实际编程当中是非常有用的功能,具体场景如:我们想修改JDK中的String,想在它的基础上增加一个方法"lastElement() "来获取末尾元素,如果使用Java,我们是无法通过常规手段实现的,因为我们无法修改JDK的源码。任务第三方提供的SDK,我们都无权修改。
但,借助Kotlin的扩展函数,我们就可以完全在语法层面,来为第三方SDK的类扩展新的成员方法和成员属性。不管是为JDK的String增加新的成员方法,还是为Android SDK的View增加新的成员属性,我们都可以实现。
什么事扩展函数和扩展属性?
扩展函数,就是从类的外部扩展出来的一个函数,这个函数看起来就像是类的成员函数一样。这里,我们以JDK中的String为例,来看看如何通过Kotlin的扩展特性,为它新增一个lastElement()方法。
// Ext.kt
package com.tcl.demo
fun String.lastElement(): Char? {
if (this.isEmpty()) {
return null
}
return this[length - 1]
}
// 使用扩展函数
fun main() {
val msg = "Hello Wolrd"
// lastElement就像String的成员方法一样可以直接调用
val last = msg.lastElement() // last = d
}
如代码所示,我们先是定义了一个String的扩展函数"lastElement()",然后在main函数当中调用了这个函数,并且,这个扩展函数直接定义在Kotlin文件里的,而不是定义在某个类当中,这种扩展函数,我们称之为“顶层扩展”,因为它并没有嵌套在任何类当中,它自身就在最外层。
其中,"String.",代表我们的扩展函数为String这个类定义的,在Kotlin当中,它有一个名字,叫做接收者(Receiver),也就是扩展函数的接收方。
此外,如果去掉"String.",这段代码就会变成一个普通的函数定义:
fun lastElement(): Char? {
if (this.isEmpty()) {
return null
}
return this[length - 1]
}
反之,如果在普通函数的名字前面加上一个“接受者类型”,如"String.",Kotlin的普通函数就变成了扩展函数。
可见,Kotlin的扩展语法设计的非常巧妙,只要记住了普通函数的语法,只需要再记住一点点细微的差别,就能记住扩展函数的语法,所谓的扩展函数,就是多了个“扩展接受者”的函数。
扩展函数的实现原理
反编译示例代码,
public final class ExtKt {
public static final Character lastElement(String $this) {
CharSequence var1 = (CharSequence)$this;
if (var1.length() == 0) {
return null
}
return var1.charAt(var1.length() - 1);
}
}
public static final void main() {
String msg = "Hello Wolrd";
Character last = ExtKt.lastElement(msg);
}
可查看到:原本定义在String类型上面的扩展函数lastElement(),变成了一个普通的静态方法。另外,之前定义的扩展函数lastElement()是没有参数的,但反编译后的Java代码中,lastElement(String $this)多了一个String类型的参数。原本msg.lastElement()的地方变成了ExtKt.lastElement(msg),这说明,Kotlin编写的扩展函数调用代码,最终会变成静态方法的调用。
也就是说,由于JVM不理解Kotlin的扩展语法,所以Kotlin编译器会将扩展函数转换成对应的静态方法,而扩展函数调用处的代码也会被转换成静态方法的调用。
如何理解扩展属性?
扩展函数,是在类的外部为它定义一个新的成员方法;而扩展属性,则是在类的内部为它定义一个新的成员属性。
在反编译Kotlin代码之后,我们知道,从外部定义的成员方法和书写,都只是语法层面的,并没有实际修改那个类的源代码。
还是以lastElement为例,以扩展属性的方式实现。扩展函数的定义比普通函数,其实只是多了一个“接收者类型”,类似的,扩展属性,也就是在普通属性定义的时候多加了一个“接收者类型”即可。
// 接收者类型
val String.lastElement: Char?
get() = if (isEmpty()) {
null
} else {
get(length - 1)
}
fun main() {
val msg = "Hello Wolrd"
// lastElement就像String的成员属性一样可以直接调用
val last = msg.lastElement // last = d
}
在示例代码中,我们为String类型扩展了一个新的成员属性"lastElement",然后在main函数当中,直接使用msg.lastElement使用了这个扩展属性。
上面的两个箭头,说明了扩展函数与扩展属性,它们最终会被Kotlin编译器转换成静态方法;下面两个箭头,说明了扩展函数和扩展属性的调用代码,最终会被Kotlin编译器转换成静态方法的调用。
不管是扩展函数还是扩展属性,它本质上都会变成一个静态的方法。
扩展能做什么?
当我们想要从外部为一个类扩展一些方法和属性的时候,我们就可以通过扩展来实现了。在Kotlin当中,几乎所有的类都可以被扩展,包括普通类,单例类,密封类,枚举类,伴生对象,甚至包含第三方提供的Java类。唯有匿名内部类,由于它本身不存在名称,我们无法指定“接收者类型”,所以不能被扩展,当然,它也没有必要被扩展。
可以说,Kotlin扩展的应用范围是非常广的,它最重要的用处,就是用来取代Java当中的各种工具栏,比如StringUtils,DateUtils等。
所有Java工具类能做的事情,Kotlin扩展函数都能做,并且可以做的更好,扩展甘薯的优势在于,开发工具可以在编写代码的时候智能提示。
扩展不能做什么?
Kotlin的扩展,由于它本质上并没有修改接收类型的源代码,所以它的行为是无法与类成员完全一致的,对比普通的类成员,它有以下几个限制:
1、无法被它的子类重写。
2、扩展属性无法存储状态。
3、扩展的访问作用域仅限于两个地方:1)定义处的成员,2)接收者类型的公开成员。(无法访问私有成员)
针对扩展的第三个限制来说:
- 如果扩展时顶层的扩展,那么扩展的访问域仅限于该Kotlin文件当中的所有成员,以及被扩展类型的公开成员,这种方式定义的扩展是可以被全局使用的。
- 如果扩展是被定义在某个类当中的,那么该扩展的访问域仅限于该类当中的所有成员,以及被扩展类型的公开成员,这种方式定义的扩展仅能在该类当中使用。
实战与思考
在Kotlin源码当中,String.kt的源码如下:
// String.kt
public class String : Comparable<String>, CharSequence {
companion object {}
public operator fun plus(other: Any?): String
public override val length: Int
public override fun get(index: Int): Char
public override fun subSequence(startIndex: Int, endIndex: Int): CharSequence
public override fun compareTo(other: String): Int
}
你一定会惊讶,Kotlin里面的String类竟然只有不到十行代码,那么String类的那些字符操作的方法去哪里了?如,String.trim(),String.lowercase()
实际上,String相关的操作方法全部放到了Strings.kt当中去了。而这些字符操作方法全部都是以扩展函数的方式定义的:
// Strings.kt 部分代码
public fun CharSequence.trim(): CharSequence = trim(Char::isWhitespace)
public expect fun String.lowercase(): String
这就是Kotlin扩展的一个很典型的使用场景:关注点分离。所谓关注点分离,就是将我们程序的逻辑划分成不同的部分,每一个部分,都只关注自己的那部分的职责。以上面的String类为例,String.kt这个类,只关注String的核心逻辑,而Strings.kt则只关注String的操作符逻辑。
小结一下,Kotlin扩展主要有两个核心的使用场景:
1、主动使用扩展,通过它来优化软件架构。
对复杂的类进行职责划分,关注点分离。让类的核心尽量简单易懂,而类的功能性属性与方法以扩展的形式存在于类的外部,如String.kt和Strings.kt
2、被动使用扩展,提升可读性与开发效率。
当我们无法修改外部的SDK时,对于重复的代码模式,我们将其以扩展的方式封装起来,提供给对应的接收者类型。
在学习的过程中,我们需要将知识点梳理,分类整理,关联记忆,形成体系化的知识面,深度思考,不断深挖探索。