本篇文章是全书第12章,关于Kotlin的流畅性的笔记。本章内容比较杂,主要是涉及到使用Kotlin编写出质量更好的代码。本文将介绍Kotlin的拓展函数,属性注入以及一些使用kotlin编程的小技巧。
拓展函数简单来说就是在不修改一个类的前提下,为这个类添加函数(甚至包括final类)。这是因为Kotlin中虽然默认是不允许继承一个类的,但是对于拓展一个类是持开放态度的(符合开闭原则)。拓展函数和拓展属性是添加方法和属性的技术,但不会修改目标类的字节码。
不过一个类会优先寻找自身的实例方法,然后才会寻找拓展函数。当两者发生冲突的时候,类
的成员总是会胜过拓展函数。
可以写一个例子,比如我们可以定义两个数据类,为一个圆类和一个点类:
data class Circle(val x:Int,val y:Int,val radius:Int)
data class Point(val px:Int,val py:Int)
此时我们又想定义一个函数来判断这个点是否被包含在圆圈中。一般情况下我们可以有两种解决方法,分别是定义一个静态方法,或者是为其中一个类添加方法。Kotlin中就提供了拓展函数来提供解决方法。这里我们为Circle写一个拓展函数:
fun Circle.contains(point:Point):Boolean{
val dis = (point.px - x)*(point.px - x) + (point.py - y)*(point.py - y) - radius*radius
return dis <= 0
}
这是一个拓展函数,为Circle类添加了contains方法。底层的实现尽管实际上也是通过静态方法实现的,但是我们看起来起码就清晰不少,且我们可以像原生方法一样调用它:
fun main() {
val circle = Circle(100,100,25)
val point1 = Point(110,110)
val point2 = Point(10,100)
println(circle.contains(point1))
println(circle.contains(point2))
}
只要这个方法是可见的,那么我们就可以调用这个拓展函数。
前面介绍了拓展函数,既然函数都可以注入,那么属性更不用说,我们也可以为类添加拓展属性。不过由于他们实际上并不是类内部的一部分,所以拓展属性不能使用幕后字段。也就是说他们不能像实际属性那样访问字段。在上面的例子上做修改,我们为Circle拓展一个面积属性:
val Circle.area:Double
get() {
return PI * radius * radius
}
fun Circle.showDetail(){
println("圆的X坐标是:$x,Y坐标是:$y,半径是:$radius,面积是:$area")
}
这里我们还额外为Circle添加了一个拓展函数以便后续测试,我们也可以看到这个拓展属性是可以被拓展函数给调用的,并且就像调用原生属性一样。我们来看测试结果:
另外,我们也可以拓展var的属性,这样的话我们还需要编写设置器set方法。
除了注入到实例中,我们还可以将方法或者属性注入到伴生对象中,实际上和之前的是一样的。比如说:
val String.Companion.sk:Int
get() = 0
这里就不作过多赘述了。
前面我们的实例都是从文件的顶层来注入方法或者变量的,实际上我们也可以在我们自己的类中对其他类注入方法或者变量。只不过这样的话就会涉及到可见性的问题。如果在我们的类中对其他类拓展方法,那么这个方法仅在我们的类中可见,其他并没有什么太大的区别。
需要说明的是,Kotlin中本来就有很多现有的方法是拓展函数。与原生方法和拓展函数不同,我们是可以用一个拓展函数来覆盖另一个拓展函数从而达到完全改变函数行为的效果,不过千万不要这样做,比如说:
fun String.toLowerCase():String{
return this.toUpperCase()
}
这个方法就完全修改了原方法的效果(甚至是完全相反),这会引起许多问题,所以不要改变现有方法的行为。
这是一个书上的例子,我们如果想要遍历从help到helz范围内的String的话,默认情况下是不允许的,因为ClosedRange方法并没有iterator方法。不过有了拓展函数之后,我们就可以实现了。
这里我们的思路就是记录下String范围的起始字符串,hasNext方法通过比较当前String和始末String来判断是否有下一个值。next方法则是用StringBuilder作为中间类来进行迭代:
operator fun ClosedRange<String>.iterator():Iterator<String>{
return object :Iterator<String>{
val next = StringBuilder(start) //记录开始
val last = endInclusive //记录结束
override fun hasNext(): Boolean {
return last >= next.toString() && last.length >= next.length
}
override fun next(): String {
val result = next.toString()
val lastCharacter = next.last()
if(lastCharacter < Char.MAX_VALUE){
next.setCharAt(next.length - 1,lastCharacter + 1)
}else{
next.append(Char.MIN_VALUE)
}
return result
}
}
}
不过需要说明的是,这个例子仅适用于起始范围只差最后一个字母的情况,比如当范围为"hekp"…"helz"时这个方法就会失效,不过这里还是对“help”到“helz”进行测试:
成功运行。
其实在这里我们要看的就是四个重要的方法,分别是also(),apply(),let()和run()。每个方法都接受一个lambda表达式作为参数,并在调用给定的lambda后返回一些内容。
let方法就是将调用方传递给lambda表达式的参数,比如:
fun main() {
val str = "NAME"
val result = str.let { println(it)
"result"
}
println(result)
}
这段代码的输出是:
因为let函数将调用方str作为参数传递给了lambda表达式中的it,这个lambda表达式也可以返回参数,这个lambda返回的就是最后一行的数据,比如说上文的result。
我们之将let改为also,观察其输出:
fun main() {
val str = "NAME"
val result = str.also { println(it)
"result"
}
println(result)
}
结果为:
可以看到,与let函数所不同的是,also函数将不会返回lambda表达式最后的结果,它返回的也是调用者本身。在这里返回值为str。
接下来略微修改代码:
fun main() {
val str = "NAME"
val result = str.apply { println(this)
"result"
}
println(result)
}
输出结果为:
可以看到,和also方法一样,apply方法忽略lambda表达式最后的返回值,并将调用方作为最后的结果返回。并且apply方法不接受参数,而是将调用方绑定到后面的this上。
其实通过上面三个方法,我们排列组合也差不多可以推断出run方法的效果,继续修改代码运行:
fun main() {
val str = "NAME"
val result = str.run { println(this)
"result"
}
println(result)
}
结果为:
可以看到run方法也是将this与调用方进行绑定,并且其返回的是lambda表达式最后的返回值。
根据这四个函数各自不同的效果,书上也给出了一些建议来优化我们的代码。不过Coding毕竟还是要自己实践,这里简要介绍这些提示: