天马行空-Kotlin扩展篇

274 阅读7分钟

扩展

Kotlin中的扩展也是这个语言吸引人的一部分,在大多数时候我们可以利用扩展的魔法来实现一些非常炫酷的功能。比如说下面这段代码。

val Int.dp: Int
    get() {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            this.toFloat(),
            Resources.getSystem().displayMetrics
        ).toInt()
    }

它允许我们使用 10.dp 这样的书写形式将一个整数所表示的dp值转化为px值。而Kotlin的扩展不止体现在扩展属性方面,它同样也可以扩展方法,如下面这一段代码就展示了如何返回字符串重复N次的结果。

fun String.repeatN(cnt: Int):String {
    val sb = StringBuilder()
    for (i in 1..cnt) {
        sb.append(this)
    }
    return sb.toString()
}

fun main() {
    println("abc".repeatN(10))
}

仔细观察这个函数的定义,与普通函数不同的是,在函数名之前有一个数据类型的声明。它指定了对什么类进行扩展,如上文代码就是对String类进行扩展,那么字符串类型就可以调用这个函数。而在函数内部,我们可以使用 this 拿到调用的对象。
看起来这种操作非常炫酷,像是修改了类,给类加了额外的方法一样,但实际上背后也没有什么黑魔法,也是通过Java的操作来实现的,反编译后的Java如下(已精简)

public static final String repeatN(@NotNull String $this$repeatN, int cnt) {
      StringBuilder sb = new StringBuilder();
      int i = 1;
      int var4 = cnt;
      if (i <= cnt) {
         while(true) {
            sb.append($this$repeatN);
            if (i == var4) {
               break;
            }
            ++i;
         }
      }
      return sb.toString();
   }

   public static final void main() {
      System.out.println(repeatN("abc", 10));
   }

可以看到,扩展函数在Java中与普通函数没什么两样,调用的时候,目标对象当做参数传递给了函数,所以它才有了对象的上下文。
此外,你可以清晰的看到,扩展函数是静态的。这也就意味着,如果你尝试用多态下调用函数的那一套思维来调用扩展函数是不可以的。

fun Shapes.print()= print("this is shapes ")
fun Rings.print()= print("this is rings ")
open class Shapes
class Rings:Shapes()

fun main() {
    val ring:Shapes = Rings()
    val ring2:Rings = Rings()
    ring.print()
    ring2.print()
}

//输出: this is shapes this is rings

如上述代码,在我们尝试用Shapes类型调用扩展的时候,输出的是Shape的内容。实际上你已经能想象到反编译后的情形。

public static final void print(@NotNull Shapes $this$print) {
    String var1 = "this is shapes";
    System.out.print(var1);
}

public static final void print(@NotNull Rings $this$print) {
    String var1 = "this is rings";
    System.out.print(var1);
}

public static final void main() {
    Shapes ring = (Shapes)(new Rings());
    Rings ring2 = new Rings();
    print(ring);
    print(ring2);
}

这种特性,叫做静态解析,这也说明了扩展函数仅仅是赋予了对象一种可以调用此函数的能力,并没有真正的将此函数编译到原始的类中。


扩展属性

开篇已经见识到了扩展属性的强大,但是不知道你注意到没有,扩展字段同扩展函数一样,并没有将字段添加到目标类中。因此我们不能直接使用或者初始化这个属性。

Int.pow = 1   //错误的

甚至,定义的时候也不能直接赋值。

image.png

image.png

因为他没有幕后字段来帮助初始化,所以我们需要手动指定getter。实际上,在我们指定get函数之后,扩展属性也变成了类似于扩展方法的样子,反编译后如下。

 public static final int getPow(int $this$pow) {
      return $this$pow * $this$pow;
   }

   public static final void main() {
      int var0 = getPow(10);
      System.out.println(var0);
   }

因此,扩展属性只是得益于初始化器的另一种便捷的扩展函数实现。


伴生对象的扩展

伴生对象在Kotlin中也被赋予了扩展的能力。

//官方demo
class MyClass {
	companion object { } // 将被称为 "Companion"
}
fun MyClass.Companion.printCompanion() { println("companion") }
fun main() { 
    MyClass.printCompanion()
}

定义和使用大同小异。


深入理解扩展

扩展函数已经被“扒了裤子”,为什么还要说深入理解扩展呢?这是因为我们要讨论一个更复杂的话题。
通常情况下,我们将扩展函数写成顶层声明,这样我们在使用的时候可以直接使用(或者导包后使用),但是如果我们将其定义在一个类中呢?还是可以直接调用么?那如果这样操作,会发生什么呢?
在聊这部分内容的时候,我们首先约定 包含一个扩展函数的类的实例叫做“分发接收者”,而扩展函数签名中 类型.函数名 部分里的类型的实例叫做“扩展接收者”。
下面我们来看一些代码,来探究一下扩展函数中到底还有什么值得深究的。

上下文

示例参考自官方文档。

class Host(val hostname: String) {
    fun printHostname() {
        print(hostname)
    }
}

class Connection(val host: Host, val port: Int) {
    fun Host.getConnectionString() {
        toString() // 调用 Host.toString()
        this@Connection.toString() // 调用 Connection.toString() 
    }
}

在这个代码中,分发接收者为Connection, 扩展接收者为Host,当我们尝试在扩展函数中调用成员函数的时候,优先调用到的是扩展接收者的成员函数,这也比较好理解,因为这个函数是扩展给 Host的,优先调用 Connection的成员函数好像也没什么道理。如果我们真正想要调用分发接收者的成员函数时,我们可以通过@语法来调用,这种方式是使用外部类的常见方式。


要点:扩展函数中,默认拥有扩展接收者的上下文。无论此扩展函数是顶层声明还是包含在分发接收者内部。

上面总结出一条规则,看起来非常好理解。下面继续看。

class Connection(val host: Host, val port: Int) {
    fun printPort() { print(port) }
    fun Host.getConnectionString() {
        printHostname() // 调用 Host.printHostname()
        printPort()
    }
}

Connection中定义了一个新的函数,而其中的扩展函数可以直接调用,更不必说,这个扩展函数可以直接调用Host中所定义的函数。当我们反编译Connection的代码之后,我们发现了非常惊讶的一幕(戏真多)

public final class Connection {
   public final void getConnectionString(@NotNull Host $this$getConnectionString) {
      $this$getConnectionString.printHostname();
      this.printPort();
   }   
}

定义在其中的Host的扩展函数,其实被定义为它的成员函数,而毫无疑问,在函数体中,我们可以通过参数访问Host的成员函数,也可以通过this访问分发接收者的成员函数。
虽然我们看到扩展函数被编译成public的成员函数,但是使用Connection访问总归是无意义的,所以Kotlin限制在分发接收者外部无法访问其内部的扩展函数。当然内部访问是可以的。

class Connection(val host: Host, val port: Int) {
    fun get(){
        host.getConnectionString()
    }
}

要点:分发接收者中定义的扩展函数并不能在分发接收者外部访问,尽管他被编译成分发接收者的成员函数。

继承关系

扩展函数也可以被继承,其实我们都可以猜出来,因为刚才我们已经看到了定义在分发接收者里的扩展函数,被编译成了分发接收者的成员函数,既然是成员函数,那么就有被继承的可能性。请看下面例子

例子代码来源于官方文档


open class Base {}
class Derived : Base() {}
open class BaseCaller {
    open fun Base.printFunctionInfo() {
        println("父类Base扩展")
    }

    open fun Derived.printFunctionInfo() {
        println("父类Derived扩展")
    }

    fun call(b: Base) {
        b.printFunctionInfo() // 调用扩展函数
    }
}

class DerivedCaller : BaseCaller() {
    override fun Base.printFunctionInfo() {
        println("子类Base扩展")
    }

    override fun Derived.printFunctionInfo() {
        println("子类Derived扩展")
    }
}

我们定义了 BaseDerived ,并给他们在 BaseCallerDerivedCaller里添加了扩展函数,但是值得注意的是,后者所添加的扩展函数是重写自其父类BaseCaller的,这也印证了扩展函数可以被重写的一点。具体的反编译Java源码也不必放上来了,因为与之前的类似。
那么此时,我们再用如下形式调用。

fun main() {
    BaseCaller().call(Base())
    DerivedCaller().call(Base()) // 分发接收者虚拟解析
    DerivedCaller().call(Derived()) //扩展接收者静态解析
}

其中第一行,用的分发接收父类调用的call,传入的也是Base(扩展接收者父类),那么其打印结果一定是“父类Base扩展”,而第二行,用了分发接收者子类调用,此时表现的是分发接收者的多态特性,打印结果是 “子类Base扩展”,最后一行,我想大家都猜到了,由于扩展函数不具有多态特性,call函数上的Base 参数依然表现为Base类,所以打印结果仍是“子类Base扩展”。


要点:以分发接收者的角度来看扩展函数,满足多态特性,而以扩展接收者的角度来看扩展函数,是不满足多态特性的。