Kotlin边用边学:别把Extension Function玩坏了

Key Takeaways(划重点)

  • 设计原则不要忘
  • 扩展是个补锅匠
  • 定向输出是好手
  • 升级外挂要当心

这里假定你对Kotlin的Extension Functioins(扩展函数)有一定了解,如果没有,请先浏览官网有关它的定义和常见用法。

面向对象编程

在Kotlin的官方网站介绍扩展函数的时候,使用的例子是Collections.swap(List, int, int),那我们也拿它来说事吧。

这个是swap在Java中的定义:

Collections.java

public class Collections {
    /** @since 1.4 */
    public static void swap(List list, int i, int j)
}

看到这个,我就琢磨一个事,为什么我不能直接把这个方法定义在List上呢,好比这样:

List.java

public interface List extends Collection {
    void swap(int i, int j)
}

如果这样可行,哪里还要Collections.swap啊。

但现实是这个Collections.swap()是从Java 1.4就有的,而想要List能够原生支持swap得等到Java 8的Interface Default Methods:

public interface List extends Collection {
    void swap(int i, int j) {
        set(i, l.set(j, get(i))); // 不是Java 8+编译报错
    }
}

所以,之前我们认为的别扭设计,不是Java的设计师没想到,而真是时机未到。

那如果我们在Kotlin中也来这么玩一下(当然是用扩展函数而不是工具类了):

fun MutableList.swap(i: int, j: int): Unit

从使用的角度,和原生函数是一样的:mutableListOf(1, 2, 3).swap(0, 2)。但这个时候我们就需要问一句,为什么不直接使用原生的实现呢?

所以,基于设计原则的原生函数是始终优先于扩展函数的。(千万别忘了OO的多态的强大,而扩展函数恰恰由于是静态调用,不支持多态的)

扩展函数是个补锅匠

Kotlin自身就有很多扩展函数,譬如Array.map(transform: (T) -> R): List。这个扩展可以让数组按需转换,也是一个高频使用的扩展函数。

public inline fun  Array.map(transform: (T) -> R): List {
    return mapTo(ArrayList(size), transform)
}

看到这里可能就有疑问,为什么不做成Array的原生函数呢?

其实,如果阅读代码够仔细,可以看到,接口定义和实现中分别出现了ListArrayList,这些代表着依赖。Array本身其实是不应该依赖他们的,如果把这个map功能进行原生实现,就存在接口污染问题(高内聚,低耦合设计原则)。

所以这个时候,扩展函数就可以很好的补上这个锅 了。

特定场景的定向扩展

谈到原生函数,一般而言只会存放普遍需要的。对于只适用于某些特定场合/环境/上下文的,就不适合了。譬如Domain Object或OOA的Entity,如Person,在某些场景需要输出成json的时候,如果可以直接person.toJson()会很便捷。但这个不是基础需求,也不希望引入JSON带来第三方类/库。另外这个口子开了,后面其他使用者需要输出成xml的时候,同样会再次引入xml而带来更多的第三方类/库。

这样原本的一个存粹的Domain Object或Entity或者说我们的common/core module,依赖了一堆堆第三方包,咋看都怪怪的。

这个时候,扩展函数又是一个很好的输出。在需要的应用中,我们可以自行扩展:

// app: JSON app
fun Person.toJson(): String
// app: XML app
fun Person.toXml(): String

原生函数执行优先级高于扩展函数

对第三方库进行扩展的时候,需要注意扩展函数和原生函数在执行上的一个先后顺序:原生函数优先于扩展函数。

If a class has a member function, and an extension function is defined which has the same receiver type, the same name and is applicable to given arguments, the member always wins

这个问题在升级被扩展的类库的时候尤其需要注意。拿上面例子中的toJson来说,本来原生没提供,所以我们扩展了。但对方可能也意识到这个蛮有用(甭纠结上面不是说违反原则吗),也就在某次版本中添加了这个功能。问题是对方的原生实现在null值处理的时候和你不一样(假如对方null的时候String会是空串 "name": "",而你是不输出),就可能导致升级之后由于原生函数优先执行,你的程序可能就会挂了。

所以,玩外挂每次升级都需要额外注意

总结下扩展函数的使用要点/场景:

  • 设计原则不要忘
  • 扩展是个补锅匠
  • 定向输出是好手
  • 升级外挂要当心

希望这篇博文能对你有所帮助,喜欢的话点个赞吧!

更多Kotlin的实用技巧,请参考《Kotlin边用边学系列》

参考资料

  • The Ugly Truth about Extension Functions in Kotlin
  • More Kotlin Extension Fun

你可能感兴趣的:(Kotlin边用边学:别把Extension Function玩坏了)