自2017年google宣布Kotlin为Android官方开发语言之后,Kotlin因为其简洁、安全、互操作性和工具友好的优点,逐渐获得较多Android开发人员的青睐,并在项目中运用它。对于一种语言的学习,除了基本的使用和实践之外,我们还需要深入的去了解它的原理,这里我通过Decompile Kotlin Bytecode的方式,向大家分享Kotlin是如何实现扩展特性的。
Kotlin的扩展是什么
Kotlin 可以对一个类的属性和方法进行扩展,且不需要继承或使用装饰器模式。扩展是一种静态行为,对被扩展的类代码本身不会造成任何影响。
我们可以给String加上一个isNullString的方法,来判断一个字符串是否是类似"null"这样的。也可以给List类增加一个lastIndex的属性。这边我创建一个名为sample.kt文件,在上面写下以下代码:
package com.kingpei.kotlinextendtion
import android.text.TextUtils
fun String.isNullString():Boolean{
return this.toLowerCase() == "null"
}
val <T> List<T>.lastIndex: Int
get() = size - 1
这样先忽略作用域问题,我们就可以直接在其他地方使用string.isNullString(text)方法和list.lastIndex属性。
扩展的原理
基本实现
让我们将kotlin编译后的字节码反编译成Java代码看看。通过AndroidStudio的Tools->Kotlin->Show Kotlin Bytecode,从弹出的窗口中点击Decompile,得到以下代码:
package com.kingpei.kotlinextendtion;
//...忽略
public final class SampleKt {
public static final boolean isNullString(@NotNull String $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
String var10000 = $receiver.toLowerCase();
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toLowerCase()");
return Intrinsics.areEqual(var10000, "null");
}
public static final int getLastIndex(@NotNull List $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
return $receiver.size() - 1;
}
上面的代码可以看得出来,实际上不管是扩展方法还是扩展属性,Kotlin都在扩展代码所编写的地方新增了一个公共方法,在Kotlin中如果不是在类中定义的方法,会自动创建一个文件名+Kt的类。在反编译的Java代码中,增加的扩展方法,都是在其他类(分发接受者)中,而不会也不可能在被扩展的类(扩展接受者)中,这就是说扩展是一种静态行为,对被扩展的类代码本身不会造成任何影响的原因。
扩展属性不允许被初始化,只能由显式提供的 getter/setter 定义。有的文章说扩展属性只能声明为val,但是从实践看也是可以声明为var,不过由于setter定义没有意义,所以跟val没有差别。(Kotlin的属性扩展实际上是通过扩展属性的getter/setter方法来实现的,setter的值没有属性来接收,所以无意义)
$receiver就是扩展方法中的扩展接受者,也是this的来源。
作用域
扩展,可以是在类之外的,也可以是在类(或伴生对象)中实现,两种情况拥有不同的作用域。上面的示例,就是在类之外,在这种情况下,扩展的属性和方法可以被其他类用。而如果是在类中实现的,该方法只能被局限在该类中,也就是说上面的情况里,如果有一个类class Sample,并在其中扩展String.isNullString(text)方法和List.lastIndex属性,就只能在Sample中使用。
从原理推导
看得出来实际上扩展的实现非常的简单,但是从这简单的实现我们可以推导出以下几点:
- 从原理上看,我们能够知道,在类中实现的扩展方法,由于实际上是在该类中新增了一个方法,因此将能够调用该类的其他方法。
这会出现一个情况,我们仍然用class Sample的例子,如果isNullString中调用了Sample类中的一个方法名字叫做replace,而String中也有一个replace方法(参数相同),那么会使用的是哪个replace方法呢?答案是String中的replace方法,如果要使用Sample中的则需要使用this@Sample.replace
package com.kingpei.kotlinextendtion
class Sample{
fun String.isNullString():Boolean{
replace(this, "sample")
return this.toLowerCase() == "null"
}
fun replace(text:String, replaceWord: String){
text.replace(replaceWord, "")
}
}
//...忽略
public final class Sample {
public final boolean isNullString(@NotNull String $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
StringsKt.replace$default($receiver, $receiver, "sample", false, 4, (Object)null);
String var10000 = $receiver.toLowerCase();
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toLowerCase()");
return Intrinsics.areEqual(var10000, "null");
}
public final void replace(@NotNull String text, @NotNull String replaceWord) {
Intrinsics.checkParameterIsNotNull(text, "text");
Intrinsics.checkParameterIsNotNull(replaceWord, "replaceWord");
StringsKt.replace$default(text, replaceWord, "", false, 4, (Object)null);
}
}
- 在分发接受者中定义的扩展函数可以被定义为open,并被分发接受者的子类重写,基类和子类中定义的扩展函数之间有继承关系吗还是当它们被使用的时候,只是运行各自扩展的功能?很显然互不干扰,如果有继承关系的话,就必须调用super,而这会将与扩展功能无关的分发接受者引入进来。
package com.kingpei.kotlinextension
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")
}
}
fun main(args: Array<String>) {
C().caller(D()) // 输出 "D.foo in C"
C1().caller(D()) // 输出 "D.foo in C1" —— 分发接收者虚拟解析
C().caller(D1()) // 输出 "D.foo in C" —— 扩展接收者静态解析
}
// D.java
package com.kingpei.kotlinextension;
//...忽略
public class D {
}
// D1.java
package com.kingpei.kotlinextension;
//...忽略
public final class D1 extends D {
}
// C.java
package com.kingpei.kotlinextension;
//...忽略
public class C {
public void foo(@NotNull D $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
String var2 = "D.foo in C";
System.out.println(var2);
}
public void foo(@NotNull D1 $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
String var2 = "D1.foo in C";
System.out.println(var2);
}
public final void caller(@NotNull D d) {
Intrinsics.checkParameterIsNotNull(d, "d");
this.foo(d);
}
}
// C1.java
package com.kingpei.kotlinextension;
//...忽略
public final class C1 extends C {
public void foo(@NotNull D $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
String var2 = "D.foo in C1";
System.out.println(var2);
}
public void foo(@NotNull D1 $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
String var2 = "D1.foo in C1";
System.out.println(var2);
}
}
-
如果是类似android.text.TextUtils不可被实例化的类是否可以扩展?答案是不行,因为编译后的代码需要一个$receiver扩展接受者实例。
-
如果要在Java代码中调用扩展方法或者函数会怎样?那就会变得有些混乱,比如Sample的例子里你想调用String.isNullString,但是你会发现根据扩展实现的地方,你只能用SampleKt或者Sample去调用。因此扩展特性不适合于存在Kotlin和Java代码,同时可能被Java代码调用的情况下去实现。
public class UseExtension {
public void use(){
Sample sample = new Sample();
sample.isNullString("sample");
}
}
总结
首先,对于扩展的使用仍然要根据面向对象的规则去使用它。其次在使用扩展的时候,要记住扩展是一个静态行为,从Java代码看原理的实现,拥有两个最主要的局限:1,需要有扩展接受者作为参数。2,存在一个扩展分发者来承担扩展功能的实现。由于这两个局限的存在,导致其他局限性的出现。
网友评论