美文网首页kotlin入门潜修
kotlin入门潜修之类和对象篇—扩展及其原理

kotlin入门潜修之类和对象篇—扩展及其原理

作者: 寒潇2018 | 来源:发表于2018-12-05 09:53 被阅读0次

    本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

    创作不易,如有转载,还请备注。

    扩展

    扩展是kotlin提供的有别于java的新功能。扩展能够在不继承类或实现接口的情况下,扩展该类的功能。kotlin中既支持方法扩展也支持属性扩展。

    扩展方法

    扩展方法的定义是基于类型定义的,它并不依赖于具体的对象,而是依附于具体的类型。如我们要为String类添加一个扩展方法lastChar,用于获取字符串的最后一个字符,可定义如下:

    fun String.lastChar(): Char {//这里为String定义了一个扩展方法,该方法的功能就是获取字符串的最后一个字符
        return this[this.length - 1]//注意这里使用了this
    }
    //测试类,提供main方法
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                println("hello".lastChar())//注意这里,打印'o'
            }
        }
    }
    

    从上面的代码可以看出,扩展方法的定义首先和普通的方法定义是一致的,只不过方法名不一样,扩展方法名要加上具体的类型,扩展方法属于什么类型就要加上什么类型,如String.lastChar(),就在lastChar()前面加上了String类型,表示lastChar方法是String的扩展方法,任何使用String的地方都可以使用该方法。

    上面代码中,lastChar方法最后return的时候使用了this关键字,表示当前对象,那么这个当前对象是什么对象呢?实际上就是我们在调用lastChar方法的时候使用的对象,本例当中就是"hello"这个字符串对象。

    kotlin中的扩展方法还可以泛型化(后面会有文章阐述泛型),比如我们要为MutaleList<T>(kotlin中可变列表接口,类比于java中的list,本身被定义为了泛型)定义一个元素交换方法:

    fun <E> MutableList<E>.swap(index1: Int, index2: Int) {//这里为MutableList添加了泛型化的扩展方法,
        val tmp = this[index1]
        this[index1] = this[index2]
        this[index2] = tmp
    }
    //测试类,提供程序入口
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                val list = mutableListOf(1, 2, 3)
                println(list)//打印[1, 2, 3]
                list.swap(1, 2)
                println(list)//打印[1, 3, 2]
            }
        }
    }
    

    扩展方法的派发机制

    看到这个标题有点懵,什么是派发机制?如果懂java语言,可以结合java语言来看,如果不同也没有关系。这里大概阐述一下。

    面向对象的语言一般会有两种派发机制,静态派发和动态派发。所谓静态派发可以想一下方法重载,即编译时就已经确定该调用哪个方法。而动态派发更多的体现在多态上,是指调用的方法只有在运行的时候才能确认。方法重载易于理解,参数不同,传入什么样的参数就调用对应参数个数的方法即可;可运行时确认调用方法是什么意思呢?

    举个简单的例子,假如类A中有个方法叫m1,类B继承自A并复写了方法m1,那么假如我们实际对象是B,对象的类型是A,即我们通过父类型来调用方法m1的时候,该具体调用那个方法呢?如下所示:

    open class A {//类A,有个m1方法
        open fun m1() {
            println("A.m1")
        }
    }
    class B : A() {//类B继承于A,并复写了m1方法
        override fun m1() {
            println("B.m1")
        }
    }
    //测试类,程序执行入口
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                printM1(B())//注意这里,调用了printM1方法,实际传入的是B对象,而printM1接收的是类型为A(即B的父类)的参数,这么传是完全合法的,是常见的多态场景。
            }
            @JvmStatic fun printM1(a: A) {//printM1方法,接收的是父类型参数
                println(a.m1())//那么问题来了,这个到底是调用A的方法还是调用B的方法?
            }
        }
    }
    

    上面代码分析中抛出来了一个问题, println(a.m1())到底是调用了A中的方法还是B中的方法呢?通过打印结果(打印出了B.m1)我们发现,实际上调用的是B中的方法,为什么?

    这是因为,在编译的时候编译器实际上是不知道printM1(a:A)方法中a的具体类型是什么,只有在真正运行的时候才知道:原来实际上传入的是B对象,所以会去调用B中的m1方法。这就是动态派发。

    上面分析完成之后,我们来看下kotlin中扩展方法的派发机制,先看例子:

    open class A {
    }
    class B : A() {
    }
    fun A.m1() {
        println("A.m1")
    }
    fun B.m1() {
        println("B.m1")
    }
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                printM1(B())
            }
            @JvmStatic fun printM1(a: A) {
                println(a.m1())//会打印'A.m1'
            }
        }
    }
    

    上面的代码执行完成后打印出了A.m1,完全没有收到实际类型的影响。因为我们实际传入的是B对象,但是却执行了A中的m1方法,这说明kotlin中的扩展方法是静态派发的。

    如果kotlin中既存在扩展方法又存在类成员方法,且二者方法签名完全相同,那么kotlin会执行哪个方法呢?看下面这个例子:

    class A {
        fun m1() {//定义了一个类成员方法m1
            println("A.m1: member")
        }
    }
    fun A.m1() {//这里又定义了一个扩展方法,和类成员方法签名一样
        println("A.m1: extension")
    }
    //测试类,程序执行入口
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                print(A().m1())//打印A.m1: member
            }
        }
    }
    

    上面代码会打印A.m1: member,说明执行的是A类中的成员方法。其实这也是有道理的,如果优先执行扩展方法,那么用户可以随便写个同名方法就把标准库方法给覆盖掉了。

    空类型接收器(Nullable Receiver)

    看到这个标题也是一脸雾水,怎么突然冒出来一个空类型接收器?实际上我们在定义扩展方法的时候,都已经接触到receiver(接收器)了,所谓receiver(接收器)即是该扩展方法的归属,如定义扩展方法fun A.m1() {},则A就是receiver。

    照例先看个例子:

    fun Any?.toString(): String {//为Any?增加一个扩展方法toString,这意味着调用对象是可为null的
        if (this == null) return "test"
        return toString()
    }
    //测试类,程序执行入口
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                println(null.toString())//注意,这里使用了null.toString
                println(100.toString())//这里,使用了非null对象
            }
        }
    }
    

    上面的代码看起来确实很神奇,竟然可以用null来调用toString方法!实际上上面两种调用方法是完全不一样的:null.toString()实际上调用的是Any?的扩展toString方法;而100.toString实际上调用的是Any中的toString方法,而不是扩展方法。

    扩展属性

    同扩展方法一样,kotlin也支持扩展属性,但是属性扩展只能通过复写get方法完成初始化,无法直接进行初始化,示例如下:

    val Any.name: String//正确,为Any提供扩展属性,复写了get方法
        get() = "any"
    val Any.name2: String = "any"//错误,编译器会提示name2属性没有后备字段(backing field)
    

    上面代码中的注释已经大概说明了扩展属性无法直接初始化的原因,那为什么扩展属性没有后备字段呢?这是因为扩展属性并不是直接插入类中的,即不属于类,所以就没有办法获取到后备字段。只能通过getter和setter进行定义。

    伴随对象扩展

    伴随对象的扩展同普通扩展的定义一样,这里简单举一个为伴随对象扩展方法的例子,如下所示:

    class Test {
        companion object {//为Test类下定义了一个伴随对象
        }
    }
    //为Test类下的伴随对象增加m1方法
    fun Test.Companion.m1() {
        println("extension m1")
    }
    //测试类
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                Test.m1()//打印extension m1
            }
        }
    }
    

    伴随对象扩展方法,唯一需要注意的是其接收器写法是外部类名+Companion(首字母大写)即可。

    扩展的范围

    通常情况下,我们都是作为top-level级的成员来定义扩展方法或属性的,然后在使用的地方import即可。那么如果扩展定义在类中,其使用范围是怎么样呢?

    首先,在一个类中是可以为另一个类定义扩展的,对于这样的扩展,将会有多个隐式的接受器(receiver),这是因为对象的成员可以不通过限定符语法进行访问。这句话听起来很绕口,不理解没关系,先看个例子(请仔细看里面的注释):

    class A {//class A,定义了一个成员方法m1
        fun m1() {
            println("A.m1")
        }
    }
    class B {//class B,其实例被称为调度接收器(dispatch receiver)
        fun m2() {//定义了一个成员方法m2
            println("B.m2")
        }
        fun A.m3() {//在类B中定义了一个类A的扩展方法m3,这个时候A的实例被称为扩展接收器( extension receiver)
            m1()//调用A.m1,这里没有通过限定符来访问m1方法
            m2()//调用B.m2
        }
        fun test(a: A) {
            a.m3()//调用A类型的扩展方法
        }
    }
    //测试入口
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                val b = B()
                b.test(A())//diaoyongB类中的test方法
            }
        }
    }
    

    看完代码估计还是不太理解,确实,这块有点绕。首先解释下上面提到的几个名词:
    1.调度接收器(dispatch receiver):在哪个类中定义的扩展,那个类的实例就叫做调度接收器。上面我们定义的B类型的实例就是调度接收器。
    2.扩展接收器( extension receiver): 扩展方法所属的receiver的实例就是扩展接收器。上面我们定义的A类型的实例就是扩展接收器。
    3.多个隐式的接收器是什么意思?上面代码中,因为扩展函数(m3)既有扩展接收器(A)的引用,又有调度接收器(B)的引用,所以既可以调用A中的成员也可以调用B中的成员。A和B都是接收器。

    因为扩展方法既能访问扩展接收器成员又能访问调度接收器成员,那么如果二者存在一模一样的成员该怎么办?此时,我们可以通过限定符语法来访问。示例如下:

    class A {
        fun m1() {
            println("A.m1")
        }
    }
    class B {
        fun m1() {
            println("B.m1")
        }
        fun A.m3() {
            m1()//默认调用A.m1
            this@B.m1()//显示指定调用B.m1方法
        }
        fun test(a: A) {
            a.m3()
        }
    }
    //测试入口
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                val b = B()
                b.test(A())
            }
        }
    }
    

    上述代码执行完成后,打印如下

    A.m1
    B.m1
    

    这说明,如果存在冲突的时候,会优先调用扩展接收器的方法,可以通过显示指定调度接收器来调用调度接收器中的方法。

    kotlin中扩展方法是可以被定义为open类型的,这就意味着子类可以复写扩展方法,那么如果遇到这种情况kotlin的调用机制又是怎样的呢?看个例子:

    open class A {}
    class SubA : A() {}//定义A类的子类SubA
    
    open class B() {
        open fun A.m1() {//在B类中定义了A类的扩展
            println("A.m1 in B")
        }
    
        open fun SubA.m1() {//在B类中定义了SubA类的扩展
            println("SubA.m1 in B")
        }
    
        fun test(a: A) {//测试方法入口
            a.m1()
        }
    }
    
    class SubB : B() {//定义B类的子类SubB
        override fun A.m1() {//复写了父类B中的A类扩展方法m1
            println("A.m1 in SubB")
        }
    
        override fun SubA.m1() {复写了父类B中的SubA类扩展方法m1
            println("SubA in SubB")
        }
    }
    //测试入口
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                B().test(A())//打印'A.m1 in B',很简单,调用B类中的test方法,test方法执行A类的扩展方法m1所以打印'A.m1 in B'
                B().test(SubA())//打印'A.m1 in B',因为扩展方法是静态派发的,所以test方法入参是什么类型就会调用该类型的方法,故打印结果同上面一样。
                SubB().test(A())//打印'A.m1 in SubB',SubB继承自B,故调用B类中的test方法,然后调用A的扩展方法,但是SubB此时复写了定义在B类中的A类扩展方法,所以在运行的时候会调用该SubB中的方法。
                SubB().test(SubA())//打印同上,分析也同上
            }
        }
    }
    

    代码中已经给到了详细分析,这里不再重复。只有一点,对于继承,可以认为扩展方法和普通方法一致。都会在运行时调用子类的实现(如果子类没有复写则调用父类实现)。

    扩展可见性

    对于扩展方法,kotlin采用和普通方法一样的可见性修饰规则。可以参见kotlin入门潜修之类和对象篇—权限修饰符

    kotlin为什么要提供扩展

    kotlin提供扩展的初衷就是为了解决java中一个常见的痛点。比如我们经常会写一大堆util类如StringUtil、FileUtil等等,包括java中的jdk都大量使用了这些util,如java中的Collections辅助类,先看段代码:

    import java.util.Collections;
    Collections.swap(list, Collections.binarySearch(list
                    , Collections.max(anthoerList)), Collections.max(list));
    

    上面代码的功能是先查找list是否包含有anthoerList中的最大值,然后和list中的最大值进行交互。

    这里且先不谈代码的意义,从表面上看,这段代码是不是显着很啰嗦?Collections作为一个util类到处被引用。有朋友说可以直接导出静态类啊,这样就不用每次都带Collections了?

    确实是这样,java本身支持直接导入static类,这样做以后代码的调用就可以简洁如下:

    import static java.util.Collections.binarySearch;
    import static java.util.Collections.max;
    import static java.util.Collections.swap;
    //真正调用代码
    swap(list, binarySearch(list
                    , max(athoerList)), max(list));
    

    从上面代码可以看出swap方法的调用确实简洁不少,而import却急剧膨胀。而且语义已经模糊了,因为你不太容易知道swap到底是在哪儿定义的。这也是使用java不推荐static导入的原因。

    那么理想的调用方式该是怎么样的呢?比如下面的调用方式:

    //假如java代码这么来写
    list.swap(list, binarySearch(list)
                    , max(athoerList), max(list));
    

    上面代码的写法就很简洁,语义也比较清楚:一看就知道,这是调用了list中的swap方法,目的是对list的某些元素进行swap等等。

    该写法是达到了简洁代码的目的,但是我们又不想在list这个类中去额外增加诸如swap、binarySearch等方法,该怎么办?

    kotlin正式基于上面缺陷和考虑,设计了扩展机制。

    扩展的实现原理

    本章节我们来看下kotlin中扩展的实现原理。

    首先我们在Test.kt中定义一个ExtensionClass类,并为其添加一个getClassName的扩展方法,如下所示:

    //注意,ExtensionClass位于Test.kt文件中
    class ExtensionClass {
    }
    //这里为ExtensionClass定义了一个扩展方法getClassName
    fun ExtensionClass.getClassName(){}
    

    好了,代码已经写好了,那么从何处下手来了解扩展的实现原理呢?当然还是字节码文件。kotlin的代码最终都会编译成字节码文件,我们只需要看看getClassName的字节码实现即可明白,如下所示:

    // ================com/juandou/mediapikcer/TestKt.class =================
    // class version 50.0 (50)
    // access flags 0x31
    public final class com/juandou/mediapikcer/TestKt {//注意这里是TestKt类,并不是ExtensionClass类。
      // access flags 0x19
      public final static getClassName(Lcom/juandou/mediapikcer/ExtensionClass;)V//这里就是我们所定义的扩展方法!
        @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
       L0
        ALOAD 0
        LDC "receiver$0"
        INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
       L1
        LINENUMBER 6 L1
        RETURN
       L2
        LOCALVARIABLE $receiver //这里定义了receiver
    Lcom/juandou/mediapikcer/ExtensionClass; L0 L2 0
        MAXSTACK = 2
        MAXLOCALS = 1
    
      @Lkotlin/Metadata;(mv={1, 1, 13}, bv={1, 0, 3}, k=2, d1={"\u0000\u000c\n\u0000\n\u0002\u0010\u0002\n\u0002\u0018\u0002\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002\u00a8\u0006\u0003"}, d2={"getClassName", "", "Lcom/juandou/mediapikcer/ExtensionClass;", "app_debug"})
      // compiled from: Test.kt
    }
    

    从上面字节码可以得知如下结论:

    1. kotlin中的扩展方法,实际上会被编译为public final static的方法。
    2. 该public final static方法的位置并不是位于接收器类中(本例子接收器类即是ExtensionClass),而是位于其书写位置所对应的类中(本例子即是TestKt类,见字节码)。
    3. 为类添加扩展方法后,编译器会将该类指定为扩展方法的接收器。

    那么有朋友可能会有疑问,既然编译成的方法位于书写位置对应的类中(即TestKt类),为什么我们还能通过接收器类(即ExtensionClass类)来调用呢?而不是通过书写位置对应的类调用呢?

    比如我们调用上面的getClassName方法是通过如下方式:

       ExtensionClass().getClassName()
    

    而不是:

    TestKt().getClassName()
    

    这个该如何解释呢?很简单,我们写一个调用扩展方法的示例,看下其对应的字节码就会明白了,示例如下:

    class ExtensionClass {
    }
    
    fun ExtensionClass.getClassName(){}
    //注意这里我们调用了ExtensionClass的扩展方法getClassName
    fun test(){
        ExtensionClass().getClassName()
    }
    

    我们只需看getClassName调用处的字节码,如下所示:

    // access flags 0x19
      public final static test()V
       L0
        LINENUMBER 9 L0
        NEW com/juandou/mediapikcer/ExtensionClass//new一个ExtensionClass对象
        DUP
        INVOKESPECIAL com/juandou/mediapikcer/ExtensionClass.<init> ()V//调用ExtesionClass构造方法
        INVOKESTATIC com/juandou/mediapikcer/TestKt.getClassName (Lcom/juandou/mediapikcer/ExtensionClass;)V//!!!注意这里,是通过TestKt.getClass
    Name类调用的!
       L1
        LINENUMBER 10 L1
        RETURN
       L2
        MAXSTACK = 2
        MAXLOCALS = 0
    

    上面展示了test方法编译后的字节码,其中的两处注释就已经解开了前面我们的疑问:原来扩展方法的调用会被编译成其书写位置对应的类的静态调用(即TestKt.getClassName),kotlin只是为我们“包装”了一下而已,使得我们不必自己通过TestKt.getClassName来调用。

    这么做显然是有道理的,因为我们可以在任何文件中为一个类添加扩展,显然很多时候我们并不一定能知道扩展的书写位置,也就无法通过书写扩展所在的类来调用扩展了。

    至此,扩展的实现原理我们已经讲完。

    相关文章

      网友评论

        本文标题:kotlin入门潜修之类和对象篇—扩展及其原理

        本文链接:https://www.haomeiwen.com/subject/iiwmcqtx.html