翻译说明: 翻译水平有限,文章内可能会出现不准确甚至错误的理解,请多多包涵!欢迎批评和指正.每篇文章会结合自己的理解和例子,希望对大家学习Kotlin起到一点帮助.
原文地址: [Kotlin Pearls 6] Extensions: The Good, The Bad and The Ugly
原文作者: Uberto Barbini

前言
扩展方法(还有属性)对于Java开发者来说算是个新东西,实际上它们已经在C#中出现了很长时间,不过JVM对它们的支持首次出现在Kotlin中.
ps:扩展函数不难理解,但是在使用场景和规范上的说明和教程并不多.如果你对kotlin的扩展不太熟悉建议先看一下官方文档对扩展的说明.如果你对扩展有了解,那么这篇文章能帮助大家进一步掌握它.
经过学习庞大的Kotlin代码库和浏览开源的Kotlin代码后,我注意到在扩展的地方经常会出现一个现象就是,有些能提高代码的可读性但是有些却弄得更难理解.
这也是一个在第一次用Kotlin做团队开发是的热门讨论话题,所以我认为通过我的成功和失败经验,在这里基于扩展做个总结还是有点价值的.
如果你有不同的意见或者你有好的例子,请一定联系我.
正文
-
扩展的简单介绍
在Kotlin中有两种扩展类型:扩展函数和扩展属性.
首先让我们了解一下什么是一个扩展函数和如何在Kotlin中声明它.
fun String.tail() = this.substring(1) fun String.head() = this.substring(0, 1)我刚写了两个函数,一个返回字符串的第一个字符,另一个返回余下的字符串.
这里的重点是我们函数名(比如
tail)放在了我们希望去调用函数的类型的后面,在这个例子中是String.我们写一个测试方法大概长这样:
@Test fun `head and tail`() {t+ assertThat("abcde".head()).isEqualTo("a") assertThat("abcde".tail()).isEqualTo("bcde") }很明显,如你所料.我们只是给String添加了两个方法而已.我们把代码转成java代码再看看:
@NotNull public static final String head(@NotNull String $this$head) { String var10000 = $this$head.substring(0, 1); return var10000; }发现head变成了静态的方法,并有一个String类型的参数.那这就是一个语法糖?对,这就是个语法糖!
仍旧,扩展能提高可读性也能变得更难理解.
扩展函数类型看上去像这样:
String.() -> StringString.()左边的String我们称为函数接收者
-
泛型扩展函数
看完上面的介绍后,我们知道扩展函数可以应用到任何一个类.那有没有更多的应用场景呢?有!就是下面要说的泛型扩展函数.fun <T> T.foo() : String = "foo $this"一旦
foo在你的作用域,那么你可以用任何对象去调用这个方法,包括null.下面是证实的例子:
assertThat(123.foo()).isEqualTo("foo 123") val frank = User(1, "Frank") assertThat(frank.foo()).isEqualTo("foo User(id=1, name=Frank)") assertThat(null.foo()).isEqualTo("foo null")我们也可以通过改变泛型参数去做扩展限制.比方说我们只想让某些类和它的子类调用:
fun <T: Number> T.doubleIt(): Double = this.toDouble() * 2
这里有一点请注意,我们把泛型限定为Number还不是Number?(可空的Number).这样的话null就不是一个被这个扩展函数允许的接收者类型了.assertThat(123.doubleIt()).isEqualTo(246.0) assertThat(123.4.doubleIt()).isEqualTo(246.8) assertThat(123L.doubleIt()).isEqualTo(246.0) //assertThat(null.doubleIt()).isEqualTo(null) 编译失败!所以,如果我们想限制上面提到的
foo函数只能是非空类型的函数接收者的话,我们可以像下面这样声明:fun <T: Any> T.foo() = "foo $this" -
扩展函数在中缀表示法中的应用
如果对中缀比较陌生可以先看一下官方文档的解释 官方文档-中缀表示法
举个例子,我不太喜欢Kotlin中可空字符串的拼接.我以为的结果应该是这样
null + null == null和null + "A" == "A"但是实际在Kotlin中结果是"nullnull""和"nullA".所以我们可以通过中缀函数+扩展函数++ (在反引号中)来实现我们预期的结果:
infix fun String?.`++`(s:String?):String? = if (this == null) s else if (s == null) this else this + s通过验证,达到了语气效果:
assertThat(null `++` null).isNull() assertThat("A" `++` null).isEqualTo("A") assertThat(null `++` "B").isEqualTo("B") assertThat("A" `++` "B").isEqualTo("AB")中缀表示法对开发内部使用的DSL来说非常有用.
-
扩展属性
现在让我们看一下扩展属性是做什么的.
很简单!扩展函数让我们可以给现有的类添加方法,那么扩展属性就让我们可以给现有的类添加属性.
可能你们已经知道,Kotlin编译器会在我们访问Java Bean对象的时候生成对应属性.
public class JavaPerson { private int age; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }当我们在Kotlin中使用这个bean对象的时候,所有getters和setters都变成了属性:
val p = JavaPerson() p.name = "Fred" p.age = 32 assertThat(p.name).isEqualTo("Fred") assertThat(p.age).isEqualTo(32)通过转码可以看到,这些属性都是直接通过映射到getters和setters得到的:
L1 LINENUMBER 15 L1 ALOAD 1 LDC "Fred" INVOKEVIRTUAL com/ubertob/extensions/JavaPerson.setName (Ljava/lang/String;)V如何声明新的属性呢?我们给Java的Date类添加一个
millis属性:var Date.millis: Long get() = this.getTime() set(x) = this.setTime(x)测试代码
val d = Date() d.millis = 1001 assertThat(d.millis ).isEqualTo(1001L) assertThat(d.millis ).isEqualTo(d.time) -
扩展的应用
现在让我同个一个例子来展示如何扩展优化你的代码.FizzBuzz是一个面试会被经常问道的问题.如果你不太了解什么是FizzBuzz的话可以简单理解成一个简单算法题(写一个程序打印1到100这些数字。但是遇到数字为3的倍数的时候,打印“Fizz”替代数字,5的倍数用“Buzz”代替,既是3的倍数又是5的倍数打印“FizzBuzz”)
现在首先我们可以通过属性找出打印Fizz和Buzz的数:
val Int.isFizz: Boolean get() = this % 3 == 0 val Int.isBuzz: Boolean get() = this % 5 == 0再用我们上面定义的可空拼接String的扩展方法
++,我们可以用一行代码实现FizzBuzz:fun Int.fizzBuzz(): String = "Fizz".takeIf { isFizz } `++` "Buzz".takeIf { isBuzz } ?: toString()简单分析一下:
Int.fizzBuzz()Int类的扩展函数,函数接收者是一个Int;"Fizz".takeIf { isFizz }一个Int类型调用isFizz方法如果返回true就返回字符串"Fizz",否则null;"Buzz".takeIf { isBuzz }一个Int类型调用isBuzz方法如果返回ture就返回字符串"Buzz",反则null;++拼接左右的结果;?:toString()如果是null就调用toString.下面是测试代码:
val res = (1..15).map { it.fizzBuzz()}.joinToString() val expected = "1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz" assertThat ( res ).isEqualTo(expected) -
小结
什么时候应该用扩展什么时候不应该用扩展?
下面是我的个人看法.这个总结完全是我个人的意见,你可能有不同的看法和理解.不过这些总结也是我通过review其他人的代码总结出来的,所以应该还是有一定价值的.
据我所知,目前还可有官方的规范去说明什么时候应该用扩展不过我参照了kotlin标准库总结的下面的使用模式:扩展属性的使用模式
好的使用场景:
- 替换setters和getters
- 通过映射重命名属性的字段
- 单一字段的简单方法调用(例如
isFizz)
不好的使用场景:
-
仅仅为了在DSL中去掉方法的括号:
例如转换一个Int类型成为Duration类型5.toHours()比5.hours更让人容易理解 -
对用到多个字段的重要方法里面的属性做映射
扩展函数的使用模式:
好的使用场景:
-
一个参数的纯函数
例 String.reverse() -
类型转换
例 Map.toList(), Int.toPrice() -
接口转换
例 Map.asSequence(), User.asPerson() -
链式编程
例 T.apply{…}, T.let{…} -
两个参数的中缀表示法
例 A to B, HttpRoute bind {} -
非侵入式的给现有类来点语法糖
例 User.fullName() -
规避泛型的协变和逆变问题
例 Iterable.flatMap {…}class MyContainerClass<in T> { fun <U> map(f: (T) -> U): MyContainerClass<U>{...} }编译器会报错,因为T 前面被
in修饰符限定了,但是在map方法里面又出现在out的位置.我们可以通过把map方法改成扩展函数来解决:class MyContainerClass<in T> { ... } fun <U, T> MyContainerClass<T>.map(f: (T) -> U): MyContainerClass<U>{...}
这里属于Kotlin泛型的知识如果有不太熟悉的朋友看的不太明白的话,后期我会出一章专门讲泛型的文章
不好的使用场景
- 涉及IO线程和单例的复杂方法
例 User.saveToDb(), 8080.startHttpServer() - 会改变全局状态的方法
例 “12:45”.setTime() - 多参数方法
例 “Joe”.toUser(“Smith”, “joe@gmail.com”) - 常用类型上扩展特殊(单一范围)的方法
例 Date.isFredBirthday(), Double.getDiscountedPrice()