翻译说明: 翻译水平有限,文章内可能会出现不许确甚至错误的理解,请多多包涵!欢迎批评和指正.每篇文章会结合本身的理解和例子,但愿对你们学习Kotlin起到一点帮助.html
原文地址: [Kotlin Pearls 6] Extensions: The Good, The Bad and The Ugly java
原文做者: Uberto Barbiniandroid
扩展方法(还有属性)对于Java开发者来讲算是个新东西,实际上它们已经在C#中出现了很长时间,不过JVM对它们的支持首次出如今Kotlin中.面试
ps:扩展函数不难理解,可是在使用场景和规范上的说明和教程并很少.若是你对kotlin的扩展不太熟悉建议先看一下官方文档对扩展的说明.若是你对扩展有了解,那么这篇文章能帮助你们进一步掌握它.
算法
官方文档-扩展编程
通过学习庞大的Kotlin代码库和浏览开源的Kotlin代码后,我注意到在扩展的地方常常会出现一个现象就是,有些能提升代码的可读性可是有些却弄得更难理解.bash
这也是一个在第一次用Kotlin作团队开发是的热门讨论话题,因此我认为经过个人成功和失败经验,在这里基于扩展作个总结仍是有点价值的.app
若是你有不一样的意见或者你有好的例子,请必定联系我.
函数
扩展的简单介绍学习
在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.() -> String
String.()
左边的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标准库总结的下面的使用模式:
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泛型的知识若是有不太熟悉的朋友看的不太明白的话,后期我会出一章专门讲泛型的文章