深入理解Kotlin的扩展特性

  自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中使用。

从原理推导

  看得出来实际上扩展的实现非常的简单,但是从这简单的实现我们可以推导出以下几点:

  1. 从原理上看,我们能够知道,在类中实现的扩展方法,由于实际上是在该类中新增了一个方法,因此将能够调用该类的其他方法。
      这会出现一个情况,我们仍然用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);
   }
}

  1. 在分发接受者中定义的扩展函数可以被定义为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);
   }
}
  1. 如果是类似android.text.TextUtils不可被实例化的类是否可以扩展?答案是不行,因为编译后的代码需要一个$receiver扩展接受者实例。

  2. 如果要在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,存在一个扩展分发者来承担扩展功能的实现。由于这两个局限的存在,导致其他局限性的出现。

参考:Kotlin 扩展 | 菜鸟教程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 21,783评论 1 92
  • 困了; 睡了; 梦了…… 繁星在闪, 你我两人, 独自漫步, 低头不语。 忽然间, 你离去。 望着背影, 追去,追...
    梨子儿阅读 121评论 0 0
  • 哎呀 一到周末反而不赖床!七点半准时醒!赖了会床,然后开始收拾屋子;洗衣服;给妈妈电话;洗漱;刷鞋!主要是屋子收拾...
    最佳姐妹阅读 149评论 0 0
  • 最近两个星期有点小忙,于是答应自己的写字也停了下来。 时光不待我,总觉得时间不够,做了一件另一件事就没办法去做。写...
    流年浅梦阅读 171评论 0 0
  • 一、价格锚点法 让消费者有一个可对比的价格感知 “您愿意每年花6000元去保养您的汽车,为什么不愿意花600元去保...
    Suleka阅读 882评论 0 0