为什么需要扩展
一个新特性的出现必然是为了解决之前遗留的开发问题和提升目前开发效率。扩展函数也是如此。
首先来介绍下OOP:开放封闭原则。
软件应该是可扩展,而不可修改的。也就是对扩展开放,对修改封闭
举个栗子: 当某个三方库的功能无法满足现有业务时需要新增功能时。最简单的做法就是直接对库源码修改,但是这样违反了开放封闭原则:对源码修改。
更合理的方案是依靠扩展。Kotlin的扩展函数很显然能够优雅的解决这种问题。
扩展函数是什么
首先来看下他的使用:
代码语言:javascript复制fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
val tmp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = tmp
}
我们将MutableList叫做接受者(receivers),意思就是这个MutableList接受了这个函数,也就是给这个类扩展了这个函数。
Java中的this叫做调用者,对于普通函数来说就是该函数所属类的实例也就是调用者对象。由于这个函数是属于MutableList的,所以在这个方法体中this也就是指代的MutableList。 通俗的来说扩展函数体里面的this就是receivers的类型
扩展函数怎么用
根据上面定义的扩展函数栗子,来看下这个扩展函数的用法:
代码语言:javascript复制val list = mutableListOf(1,3,5)
list.exchange(1,5)
这里看到扩展函数是基于对象实例来调用的,如果希望使用静态的方式调用又该如何写呢?稍后讲解
扩展函数是什么
还是回到刚刚第二个话题,这次的是什么就不是简单的介绍了。之前有篇文章讲解过新技术必然离不开性能方面的考虑。因此再来讲解下他是如何实现扩展函数的,我们通过解析他的反编译字节码~~
代码语言:javascript复制public static final void exchange(@NotNull List $receiver, int fromIndex, int toIndex) {
//检查$receiver参数是否为空。receiver就是调用者
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
int tmp = ((Number)$receiver.get(fromIndex)).intValue();
$receiver.set(fromIndex, $receiver.get(toIndex));
$receiver.set(toIndex, Integer.valueOf(tmp));
}
可以看到该函数会变成一个静态不可重写的方法,并且receiver变成了第一个参数。扩展函数里的的this就是receiver参数。
public 修饰的静态方法也就是全局方法,任何地方都可以调用到(之后详细说)。 看来并没有什么神奇的地方只是将扩展函数变成了一个静态方法而已。所以性能方面是没有影响的
扩展函数在哪里可以被使用
这里首先说明下,扩展函数定义在不同的地方效果也是不一样的。
- 不定义在类中,也就是类外部 可以看到上面反编译后的扩展函数就是这种类型,被static,public,final修饰的方法会有这个特征:在同一个包中是可以共享这个扩展函数的也就是可以调用到这个扩展函数。其他包里面如果也想使用这个函数就可以import这个包中的这个函数即可。
- 定义在类中,也就是类内部 这时候诡异的事情出现了,扩展函数无法被调用。接下来看下对应的扩展函数反编译后的字节码:
public final void exchange(@NotNull List $receiver, int fromIndex, int toIndex) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
int tmp = ((Number)$receiver.get(fromIndex)).intValue();
$receiver.set(fromIndex, $receiver.get(toIndex));
$receiver.set(toIndex, Integer.valueOf(tmp));
}
可以看到失去了static关键字并且变成了外部类中的方法(和正常的方法没什么区别了),也就是其他地方调用不到了,只有该类或者该类的子类可以调用;如果失去了public关键字,那么将只有该类才能使用这个扩展函数,其子类也无法使用。
总结下,如果没有定义在类中那么该函数就是静态的大家都可以调用。如果定义在类中那么就默认属于该类和子类的普通函数,所以只有在该类和子类中使用。上面只是说了调用的地方,实际上调用还是需要使用receiver进行调用。
扩展函数的限制
前面介绍了扩展函数实现的原理并且看到了扩展函数的作用域信息,接下来分析下扩展函数在哪些场景下会被限制。
静态扩展函数
首先来回顾下普通的静态函数/变量如何定义,在Kotlin中使用伴生对象类将函数/变量定义在其中,那么该函数/变量就是静态函数/变量了。
代码语言:javascript复制class Son {
companion object {
//该变量为静态变量
val age = 10
}
}
伴生类的实现可以观察反编译后的字节码,其是定义了一个Companion的静态内部类然后再该类中定义了这些静态变量和方法
和普通函数/变量一样,扩展函数也是一样的定义方式,在伴生对象中定义扩展函数:
代码语言:javascript复制fun Son.Companion.foo() {
println("age = $age")
}
这样foo就不需要Son的实例直接可以通过Son的类名进行调用了。
这样似乎看起来没有什么问题,但是当我们需要扩展三方类的静态函数时,如果其没有用Kotlin的伴生对象指定静态方法/变量,那么该方案将无法使用,只能用实例去调用。
函数优先级
有没有想过这样一种情况:就是这个类扩展的函数名之前在这个类中就已经存在了,那么调用这个方法时,会调用扩展函数还是之前类中定义好的方法。
答案是:之前类中定义的方法、 因此:成员方法优先级高于扩展函数
this的指向
当我们在类中使用扩展函数时,在扩展函数体内想要获取当前类的this,而不是默认的扩展函数的receivers的类型的时候,我们可以指定this@类名来指向外部类。
扩展函数注意点
调用者类型是运行时类型,而接受者类型是编译时类型也就是说当扩展被生命为成员函数时具体调用哪个类的扩展方法是由它的运行时类型决定,而具体调用哪个扩展方法是根据其被定义为什么类型也就是编译时可知类型。
调用者类型也就是上面说的定义在类内部的扩展函数只有类实例才可以调用,而接受者receiver类型是扩展哪个类的类型
还是java中的规则: 重载基于编译时类型,重写基于运行时类型。
所以在编写扩展函数时需要注意
- 1.如果该扩展函数定义在类内部就是顶级函数/成员函数,不能被覆盖;(因为是基于运行时类型)
- 2.我们无法访问其接收器的非公共属性;(本质是将其变为方法的第一个参数)
- 3.扩展接收器总是被静态调度。(和重载一样)
- 4.也是最重要的一点,不要滥用扩展特性,思考好合适的接受者receivers,不要什么都往context上堆;参数简化要考虑是否有副作用
总结
Kotli n的扩展函数是非常好用的,其符合OOP原则,而且还可以扩展很多函数Google的ktx库也是基于这个功能开发了很多好用的方法。