一、扩展函数
不少现代高级编程语言中有扩展函数这个概念,Java却一直以来都不支持这个功能,Kotlin对扩展函数有了很好的支持。
扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。
比如有一个功能:一段字符串中可能包含字母、数字和特殊符号等字符,现在我们希望统计字符串中字母的数量,要怎么实现这个功能?如果按照一般的编程思维,可能会很自然的写出如下函数:
object StringUtil{
fun lettersCount(str:String):Int{
var count=0
for(char in str){
if(char.isLetter()){
count++
}
}
return count
}
}
这里先定义了一个StringUtil单例类,然后在这个单例类中定义一个lettersCount()函数,该函数接收一个字符串参数。在lettersCount()方法中,我们使用for-in循环去遍历字符串中的每一个字符。如果该字符是一个字母的话,那么计数器加1,最终返回计数器的值。
现在我们需要统计某个字符串中的字母数量时,只需要编写如下代码:
val str="ABCdsw1242"
val Count = StringUtil.lettersCount(str)
这种写法可以正常工作,这也是Java编程中的标准实现思维,但有了扩展函数之后就不一样了,我们可以使用一种更加面向思维来实现这个功能,比如说lettersCount()函数添加到String类当中。
我们先来学习一下定义扩展函数的语法结构,如下所示:
fun ClassName.methodName(param1:Int,param2:Int):Int{
return 0
}
想定义普通的函数,定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中。
接下来使用扩展函数的方式来优化刚才的统计功能。
由于我们希望向String类中添加一个扩展函数,因此需要先创建一个String.kt文件。文件名虽然没有固定的要求,但是我建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这样便于你以后查找。当前,扩展函数也是可以定义在任何一个现有类当中的,并不一定非要创建新文件。不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域。
现在在String.kt文件中编写如下代码:
fun String.lettersCount():Int{
var count=0
for(char in this){
if(char.isLetter()){
count++
}
}
return count
}
注意这里的代码变化,现在我们将lettersCount()方法定义成了String类的扩展函数,那么函数中就自动拥有了String实例的上下文。因此lettersCount()函数就不再需要接收一个字符串参数了,而是直接遍历this即可,因为现在this就代表着字符串本身。
定义好了扩展函数之后,统计某个字符串中的字母数量只需要这样写即可:
val count="ABCdsw1242".lettersCount()
这样看上去是String类中自带了lettersCount()方法一样。
扩展函数在很多情况下可以让API变得更加简洁、丰富,更加面向对象。我们再次以String类为例,这是一个final类,任何一个类都不可以继承它,也就是说它的API只有固定的那些而已,至少在Java中就是如此。然而到了Kotlin中就不一样了,我们可以向Kotlin类中扩展任何函数,使他的API变得更加丰富。比如,你会发现Kotlin中的String甚至还有reverse()函数用于反转字符串,capitalize()函数用于对首字母进行大写,等等,这都是Kotlin语言自带的一些扩展函数。
二、运算符重载
在Java中有许多语言内置的运算符关键字,如+,-,*,/,%,++,–。而Kotlin允许我们将所有的运算符甚至其他的关键字进行重载,从而拓展这些运算符和关键字的用法。
我们基本上都使用过加减乘除这种四则运算符,在编程语言里面,两个数字相加表示求这两个数字之和,两个字符串相加表示对这两个字符串进行拼接。但是在Kotlin语言中,kotlin运算符重载却允许我们让任意两个对象相加,或者进行更多其他的运算操作。
运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以实现运算符重载的功能了。但问题是在于这个指定函数是什么?这是运算符重载里面比较复杂的一个问题,因为不同的运算符对应的重载函数也是不同的。比如说加号运算符对应的是plus()函数,减号运算符对应的是minus()函数。
我们这里还是以加号运算符为例,如果想要实现让两个对象相加的功能,那么它的语法结构如下:
class obj{
operator fun plus(obj:Obj):Obj{
//处理相加的逻辑
}
}
在上述的语法结构中,关键字operator和函数名plus都是固定不变的,而接收的参数和函数返回值可以根据你的逻辑自行设定。那么上述代码就表示一个Obj对象可以与另一个Obj对象相加,最终返回一个新的Obj对象。对应的调用方式如下:
val obj1=Obj()
val obj2=Obj()
val obj3=obj1+obj2
这种obj1+obj2的语法其实就是Kotlin给我们提供的一种语法糖,它会在编译的时候被转换成obj1.plus(obj2)的调用方式。
举一个例子实现让两个Money对象相加
首先定义Money类的结构,这里让Money的主构造函数接收一个value参数,用于表示钱的金额。创建Money.kt文件,代码如下所示:
class Money(val value:Int)
定义好了Money类的结构,接下来我们就使用运算符来重载实现让两个Money对象相加的功能:
class Money(val value:Int) {
operator fun plus(money: Money):Money{
val sum=value+money.value
return Money(sum)
}
}
可以看到,这里使用了operator关键字来修饰plus()函数,这是必不可少的。在plus()函数中,我们将当前Money对象的value和参数传入的Money对象的value相加,然后将得到的和传入给一个新的Money对象并将该对象返回。这样两个Money对象就可以相加了。
我们可以使用如下代码进行测试
val money1 = Money(5)
val money2 = Money(10)
val money3=money1+money2
println(money3.value)
最终结果为15
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)
}
}
这里我们又重载了一个plus()函数,不过这次接收的参数是一个整型数字,其他代码基本一样。
那么现在Money对象就拥有了和数字相加的能力:
val money1 = Money(5)
val money2 = Money(10)
val money3=money1+money2
val money4=money3+20
println(money4.value)
打印结果为35
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.compareTo(b) |
a…b | a.rangeTo(b) |
a[b] | a.get(b) |
a[b]=c | a.set(b,c) |
a in b | b.contains(a) |
注意,最后一个a in b的语法糖表达式对应的实际调用函数是b.contains(a),a、b对象顺序是反过来的。因为a in b表示判断a是否在b当中,而b.contains(a)表示b是否包含a,因此这两种表达式是等价的。
举个例子,Kotlin中的String类就对contains()函数进行了重载,因此当我们判断“hello”字符串中是否包含“he”子串时,首先可以这样写:
if("hello".contains("he")){
}
而借助重载的语法糖表达式,也可以这样写:
if("he" in "hello"){
}
结合学习的扩展函数以及运算符重载的知识,对以下功能进行优化
fun getRandomLengthString(str:String):String{
val n=(1..20).random()
val builder=StringBuilder()
repeat(n){
builder.append(str)
}
return builder.toString()
}
其实这个函数的核心思想就是将传入的字符串重复n次,而Kotlin能够让我们使用str*n这种写法来表示让str字符串重复n次,使语法更精简。
要让一个字符串可以乘以一个数字,那么肯定要在String类中重载乘号运算符才行,但是String类是系统提供的类,我们无法修改这个类的代码。这个时候就可以借助扩展函数功能向String类中添加新函数了。
既然是向String类中添加扩展函数,那么我们还是打开刚才创建的String.kt文件,然后加入如下代码:
operator fun String.times(n:Int):String{
val builder=StringBuilder()
repeat(n){
builder.append(this)
}
return builder.toString()
}
首先,operator关键字肯定是必不可少的,然后是要重载乘号运算符,参考上面的表,如果要乘那么函数名必须是times,最后由于是定义扩展函数,因此还要在方法名前面加上String.的语法结构。在times()函数中,我们借助StringBuilder和repeat函数将字符串重复n次,最终将结果返回。
现在,字符串就有了和一个数字相乘的能力,比如执行如下代码:
val str="abc"*3
println(str)
最终打印结果是:abcabcabc。
现在我们就可以在getRandomLengthString()函数中使用这种写法了
operator fun String.times(n:Int)=str*(1..20).random()
另外,必须说明的是,其实Kotlin的String类中已经提供了一个用于将字符串重复n遍的repeat()函数,因此times()函数还可以进一步精简成如下形式:
operator fun String.times(n:Int)=repeat(n)