Kotlin进阶学习2
写在前面
本文紧接上文:Kotlin进阶学习1。在之前我们学习了一些Kotlin的特性,这次接着来学习Koltin中一些有用的特性
扩展函数
介绍
扩展函数是什么呢?扩展函数表示在即使不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。
引入和使用
看起来似乎比较难以理解,我们还是结合一个例子来说明——统计字符串中的字母数量,这段代码是很容易写的,只要编写一个单例类,然后定义一个lettersCount()函数,传入字符串,然后在里面处理并返回字母数量就好了,如下:
object StringUtil{
fun lettersCount(str:String):Int{
var count = 0
for(char in str){
if(char.isLetter()){
count++
}
}
return count
}
}
要使用这段代码,也很方便:
val str = "ABC123XYZ!@"
val count = StringUtil.lettersCount(str)
但有了扩展函数,我们可以用一种更加面向对象的思维来实现这个功能,比如把letterCount()函数添加到String类当中。
我们先来学习一下扩展函数的语法结构:
fun ClassName.methodName(param1:Int,param2:Int):Int{}
相比于定义一个普通的函数,只需要在函数名前加一个ClassName.的语法结构就可以添加到对应的类当这种去了。
接下来就来使用一下,需要注意的是我们建议把扩展函数定义成顶层方法,这样就可以作用到全局了。
比如刚才的例子,我们把这个函数定义到String类中:
fun String.lettersCount():Int{
var count = 0
for(char in this){
if(char.isLetter()){
count++
}
}
return count
}
这里定义成了String类的扩展函数,那么函数内部自然就有了String实例的上下文,这里的this就是指的这个String实例了。
此时,要使用这个扩展函数就更简单了:
val count = "ABC123456".lettersCount()
看上去就好像是String类自带了这个方法一样。
使用扩展函数,可以让API变得更加简洁,丰富,并且更加地面向对象。最后提一句,你可以向任何类中添加拓展函数,Kotlin对此没有限制。
运算符重载
介绍
记得我第一次学习C++的时候,对C++中的运算符重载机制觉得十分惊讶。居然连运算符都可以重载。非常好的是我们的Kotlin也支持运算符重载, 但需要注意的是,不同的运算符对应的重载函数是不同的,比如加号对应的是plus(),减号对应的是minus()函数,在重载函数前加上operator关键字就可以编写里面的逻辑了:
class Obj{
operator fun plus(obj:Obj):Obj{
// 处理逻辑
}
}
使用
接下来,我们就来实现一个有意义的事情:让两个Money对象相加。
首先定义一个Money类:
class Money(val value:Int)
在Money类中重载加法:
class Money(val value:Int){
operator fun plus(money:Money):Money{
val sum = value + money.value
return Money(sum)
}
}
可以看到,这段代码并不复杂,就不过多解释了。我们来使用一下:
val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2
但是Money对象只能和Money对象相加,未免有点不方便。如果能直接和数字相加的话就更好了。这个功能当然也是可以实现的,因为Kotlin允许多重重载:
class Money(val value:Int){
operator fun plus(money:Money):Money{
val sum = value + money.value
return Money(sum)
}
operator fun plus(newValue:Int):Money{
val sum = value + newValue
return Money(sum)
}
}
要使用也很简单:
val money4 = money3 + 50
语法糖表达式与实际函数对应表
Kotlin允许我们重载的运算符和关键字多达十几个,具体的表如下:
语法糖表达式 | 实际调用函数 |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b) |
a++ | a.inc() |
a-- | a.dec() |
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a == b | a.equals(b) |
a > b | |
a < b | |
a >= b | a.compareTo(b) |
a <= b | |
a..b | a.rangeTo(b) |
a[b] | a.get(b) |
a[b] = c | a.set(b,c) |
a in b | b.contains(a) |
需要重载的时候可以进行查表,不必记住全部。
高阶函数
介绍
在之前的学习中,我们学习了很多函数式API的用法,比如map,filter,run等等。这些函数的特点就是可以传入一个Lambda表达式作为参数。像这种接收Lambda参数的函数就可以称为具有函数式编程风格的API,而如果想定义自己的函数式API,就需要借助高阶函数了。
所谓高阶函数,指的是如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就被称作高阶函数。
这个概念乍一看十分奇怪,函数怎么能接收函数作为参数呢?其实在Kotlin中,除了整型,布尔型等等,还定义了函数类型。如下格式:
(String,Int) -> Unit
既然是定义函数类型,那么关键的就是要声明接收什么参数,以及返回值。->左边的部分就是用来声明接收的参数的,多个参数用逗号隔开,如果不接收则只写一对括号就好了。右边则是用于声明函数的返回值,如果没有返回值就写Unit,大概相当于Java中的void。
有了函数类型的定义后,我们就可以定义高阶函数了:
fun example(func:(String,Int) -> Unit){
func("hello",123)
}
那么高阶函数到底有什么用呢?总的来说就是高阶函数运行让函数类型的参数来决定函数的执行逻辑。接下来我们实践一下。
使用函数引用来使用
这里我们定义一个叫做num1AndNum2()的高阶函数,用来处理两个数字之间的某种运算,然后返回最终结果:
fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int) -> Int) : Int{
val result = operation(num1,num2)
return result
}
具体来看,这里的前两个参数不解释了。第三个参数,我们定义了一个名字叫做operation的函数类型参数,这个参数接收两个Int型的参数,且返回一个Int型的结果。我们在具体实现逻辑里没有进行实际的运算,而是交给了operation来运算,并返回结果。
那么我们怎么使用?如下:
fun plus(num1:Int,num2:Int):Int{
return num1 + num2
}
fun main(){
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1,num2,::plus)
println(result1)
}
为了使用这个高阶函数,我们必须先定义一个与他定义的函数类型匹配的函数才行。于是我们定义了一个plus函数,用来处理加法。之后我们在主函数里使用这个高阶函数,其中::plus代表函数引用,表示将plus()函数当作参数传递给num1AndNum2()函数。这里其实就是使用了plus函数的逻辑去决定了具体的运算逻辑。
但这种写法,每次使用高阶函数还得先定义一个普通函数,未免太过于复杂了。实际上Koltin中提供了很多种调用高阶函数的方式,比如Lambda表达式、匿名函数、成员引用等等。其中Lambda表达式是最常用的用法了。所以接下来我们重点学习这部分。
使用Lambda表达式来使用
上面的代码如果用Lambda表达式来写的话,如下:
fun main(){
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1,num2){
n1,n2 -> n1 + n2
}
println(result1)
}
可以看到,之前的代码变得十分精简了。我们也可以把刚才定义的plus()函数删掉了。
为了加深理解,我们接下来再做一个例子,尝试用高阶函数实现类似apply函数的功能。典型的就是StringBuilder,我们可以定义一个StringBuilder的扩展函数:
fun StringBuilder.build(block:StringBuilder.() -> Unit) :StringBuilder{
block()
return this
}
注意,这里的函数类型声明方式和之前不同。其实这才是定义高阶函数最完整的语法,在函数类型的前面加上ClassName代表这个函数类型是定义在哪个类中的。那么这样写有什么好处呢?好处就是当我们调用build函数时,传入的Lambda表达式将会自动拥有StringBuilder的上下文,就跟apply函数很像了吧?那么赶紧来试试:
fun main(){
val list = listOf("Apple","Banana","Orange","Pear")
val result = StringBuilder().build{
append("Start eating fruits")
for (fruit in list){
append(fruit).append("\n")
}
append("Ate all things")
}
println(result.toString())
}
可以看到,这个build函数和apply函数实现了类似的功能,不过这里的build函数暂时只能作用于StringBuilder类上面,等我们学习了泛型后就可以作用于所有类上了。
内联函数
介绍
要了解内联函数的定义和作用,就要先来看看高阶函数的具体实现。刚才我们学习了高阶函数,可以看到十分的方便,传入一个Lambda表达式就可以了。但Koltin的代码还是要编译成Java字节码的,Java又不支持高阶函数,那是怎么实现的呢?其实Kotlin的编译器会将高阶函数转换成类似于匿名内部类的实现方式。这也就代表每调用一次Lambda表达式,都会创建一个新的匿名类实例,当然会造成额外的内存和性能开销。
为了解决这个问题,Kotlin提供了内联函数的功能,它可以将Lambda表达式时带来的运行开销完全消除。
使用
要使用十分简单,加上inline关键字即可:
inline fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int) -> Int) : Int{
val result = operation(num1,num2)
return result
}
那么工作原理又是如何呢?其实就是Kotlin编译器会把内联函数中的代码在编译的时候自动替换到调用它的地方。这样一来运行时的开销也就消除了。
noinline
但是,考虑一些特殊情况,比如一个高阶函数接收了两个或更多的函数类型的参数,但我们只想内联其中的一个怎么办呢?这时就可以使用noinline关键字了:
inline fun inlineTest(block1:() -> Unit,noinline block2: () -> Unit){
}
这样,只有block1()会内联,而block2()则不会。内联函数与非内联函数,有一个重要区别,那就是内联函数内引用的Lambda表达式是可以使用return 关键字来进行函数返回的,但非内联函数却不能。
crossinline
将高阶函数声明成内联函数是一种良好的编程习惯。事实上,绝大多数的高阶函数都是可以直接声明成内联函数的。但在有些情况下,却有例外:
inline fun runRunnable(block:() -> Unit){
val runnable = Runnable{
block()
}
runnable.run()
}
这段代码在加上了inline关键字后,就无法正常工作了。为什么呢?这里要解释起来可能有些复杂。首先,在runRunnable()函数中,我们创建一个Runnable对象,并在Runnable的Lambda表达式里调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说上述代码实际上是在匿名内部类中调用了传入的函数类型参数。
而内联函数所引用的Lambda表达式允许使用return关键字进行返回,但由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此就出了上面的错误。
总的来说,如果我们在高阶函数中创建了另外的Lambda表达式或者匿名类的实现,并且在这些实现中调用函数类型参数,就无法声明为内联函数。
要解决这个问题,可以使用crrosinline关键字:
inline fun runRunnable(crossinline block:() -> Unit){
val runnable = Runnable{
block()
}
runnable.run()
}
这个crossinline关键字,在这里的作用就像是提供了一个契约,保证在内联函数的Lambda表达式中一定不会使用return关键字了。声明了之后,就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了。总的来说,除了在return关键字上使用有所区别外,crossinline保留了内联函数的其他所有特性。
总结
总的来说,学习了很多知识。其中高阶函数要明显难理解,日后会多敲来加强理解。