开启生长之旅!这是我参与「日新计划 2 月更文挑战」的第2天,点击检查活动详情

约好是kotlin语法当中必不行少的一部分,能够说kotlin语法的整洁,约好在里边扮演着不行缺少的角色,无论是在运算,比较,解构或许调用lanmda表达式上,都能看见约好的身影

二元运算符的约好

java8里边的时刻api咱们必定不陌生,假如有不熟悉的能够看下我的这篇文章JAVA8中新的日期时刻处理办法究竟香不香,里边有对一些常用api做过详细阐明,这些api关于时刻日期操作上现已做了很大的优化,用法也特别简单,比方你想得到两天后的日期,你能够这么做

val twoDaysLater = LocalDate.now().plusDays(2)
println(twoDaysLater.format(DateTimeFormatter.ISO_DATE))
.......
2023-01-29

很简单明晰,plusDays一看就知道是做什么工作的,可是kotlin里边还能够做的更简单,比方像上面那样的代码咱们还能够这样子写

val twoDaysLater = LocalDate.now()+2
println(twoDaysLater.format(DateTimeFormatter.ISO_DATE))
.......
2023-01-29

这儿或许会有人宣布疑问了,这儿难道编译器不会报错吗?加号运算符不是只能对根底类型的变量进行操作吗?是的,在java里边的确是如此,可是在kotlin里边,咱们能够经过扩展函数跟约好完结这件工作,咱们来看下内部做了什么工作

operator fun LocalDate.plus(days:Long):LocalDate{
    val date = LocalDate.now().plusDays(days)
    return date
}

这儿给LocalDate添加了一个plus的扩展函数,让它接纳一个常量,返回另一个LocalDate目标,这儿咱们还看到运用了一个operator关键字,所以,只要运用operator关键字声明的函数,而且函数名有必要是特定的那几个函数的时分,表示你计划对这些函数做一个相应的约好去实现,咱们调用这些函数的时分才能够用运算符去替代函数名,kotlin里边以下几个函数名能够拿来做约好,用运算符去替代

运算符 函数名
+ plus
minus
* times
/ div
% mod

那这样一来咱们还能够对LocalDate添加一个minus的扩展函数,并用operator润饰,表示几天前的日期

operator fun LocalDate.minus(days:Long):LocalDate{
    val date = LocalDate.now().minusDays(days)
    return date
}
val twoDaysAgo = LocalDate.now()-2
println(twoDaysAgo.format(DateTimeFormatter.ISO_DATE))
.......
2023-01-25

一元运算符的约好

讲完二元的,咱们讲讲一元的,啥是一元运算符呢?比方i++自增,i–自减都是一元运算符,kotlin里边对这几个函数名做了约好,用operator润饰后能够用一元运算符去替代函数名

运算符 函数名
+i unaryPlus
-i unaryMinus
!i not
++i,i++ inc
–i,i– dec

那咱们测验一下给LocalDate再添加一个自增运算符,这样一来,遇到获取明日,昨日日期的场景,咱们连+1,-1都不必去写了,首先添加一个inc的扩展函数

operator fun LocalDate.inc(): LocalDate = LocalDate.now().plusDays(1)

然后咱们获取一天后的日期能够这样去写

var today = LocalDate.now()
println("今日的日期是${today.format(DateTimeFormatter.ISO_DATE)}")
println("明日的日期是${(++today).format(DateTimeFormatter.ISO_DATE)}")
......
今日的日期是2023-01-28
明日的日期是2023-01-29

复合赋值运算符的约好

关于像+=,-=这样的有运算跟赋值两步操作的运算符咱们称为复合赋值运算符,对应的约好函数是plusAssign和minusAssign,这个运算符除了能够像上面二元,一元那样操作之外,还能够针对一个集合做添加删除元素操作,比方

val dataList = mutableListOf("kotlin","java")
dataList += "c++"
dataList.forEach {
    println(it)
}
......
kotlin
java
c++

其实咱们便是给MutableList添加了一个plusAssign的函数,而且用operator去润饰它

operator fun MutableList<String>.plusAssign(element:String){
    add(element)
}

比较运算符的约好

equals

咱们一般在检查一些用户行为日志的时分,比较一些日志是否为同一个用户的行为,咱们会把用户的信息拿出来,再去比较用户的id是否持平,代码实现起来就像这样

data class People(var nickName:String,var userId:Int)
val userA = People("Coffee",12345)
val userB = People("Coffeeee",12345)
val userC = People("Coffee",54321)
println("A跟B${if(userA.userId == userB.userId)"是" else "不是"}同一个人")
println("A跟C${if(userA.userId == userC.userId)"是" else "不是"}同一个人")
......
A跟B是同一个人
A跟C不是同一个人

可是在判别用户是否为同一个人的时分,咱们一般只会以用户id为准,不会去用其他字段,那是否能够把.userId省掉?等号两边只去判别People这个目标呢?咱们只需求将People里边的equals函数重写一下,然后将它约好成咱们需求用的比较运算符就能够了

data class People(var nickName: String, var userId: Int) {
    override operator fun equals(other: Any?): Boolean {
        if (other !is People) {
            return false
        }
        return this.userId == other.userId
    }
}
val userA = People("Coffee",12345)
val userB = People("Coffeeee",12345)
val userC = People("Coffee",54321)
println("A跟B${if(userA == userB)"是" else "不是"}同一个人")
println("A跟C${if(userA == userC)"是" else "不是"}同一个人")
......
A跟B是同一个人
A跟C不是同一个人

compareTo

现在咱们来看另一个比较运算符compareTo,这次咱们给People类添加一个age的特点,运用比较运算符来比照两人的年纪大小

data class People(
    var nickName: String, 
    var userId: Int,
    var age:Int=0
)
val userA = People("Coffee",123,23)
val userB = People("Tea",125,24)
println("Coffee的岁数 比 Tea ${if(userA > userB) "大" else "小"} ")

假如直接给两个People目标用>或许<运算符,编译器是会报错的,咱们给People添加个扩展函数compareTo,让它里边对age做比照

operator fun People.compareTo(other:People) = (this.age - other.age)

这个时分编译器就不报错了,运转一下得到的成果为

Coffee的岁数 比 Tea 小

注意:假如界说的扩展函数,在规范库里边现已存在相同签名的函数,那么运算符的逻辑只会以规范库的为准,自己界说的函数里边的逻辑将会无效,由于成员函数的优先级比扩展函数要高

咱们以String为例,在kotlin规范库里边,String也有相同签名的compareTo函数,阐明就算咱们不去给String约好一个compareTo的函数,它也是能够运用>或许<这样的运算符,逻辑是逐一比较两个字符串各个字符的ASCII码值的大小,比方下面这段代码

println("123 > 32 ${"123" > "32"}")

假如不将比照的两个字符串转成整数类型,那么比照出的成果必定是false,由于它们比照的是两个字符串第一位1与3的大小,假如说咱们给String添加一个compareTo(String)的扩展函数,让它能够实现将字符串转成整数类型在比照,会有效吗?咱们试试看

operator fun String.compareTo(other:String):Int{
    return this.toInt() - other.toInt()
}

仍然仍是false,成果就不展示出来了,有爱好的小伙伴能够自己跑下试试,这个就阐明晰假如规范库里边有相同签名的函数,自己约好的函数将不起作用,逻辑以优先级高的为准,而且咱们也不必每次界说函数时分都去规范库里边找找究竟有没有相同签名的函数,由于假如有,编译器会提示你这个函数在规范库里边现已存在相同签名的了,比方上面这个compareTo,其实它是有个正告的

Kotlin系列之不知道约定,可能有些代码你看不懂

阐明这个扩展函数被成员函数给隐藏了

get与set的约好

kotlin里边对集合进行赋值或许获取一个值的时分,往往是经过list[index]这种办法来操作的,其实这个也便是集合类里边对get与set函数做了约好

Kotlin系列之不知道约定,可能有些代码你看不懂

Kotlin系列之不知道约定,可能有些代码你看不懂
而咱们能够利用这种约好,用在其他场景上,比方一个接口的数据类有若干个字断,分别运用在事务场景的各个角落,而假如有一天服务端同学告知你要更改某一个字断,咱们是不是要连着去改一切事务场景中用过这个字断的地方,有的字断用的少还好,有的字断用的地方多或许一改就要改半响,这个时分咱们能够测验着在数据类里边约好个get函数(其实这种场景应该是用Gson里边的@SerializedName,这儿便是对约好举个比方~),传进去的下标值便是拜访特点的方位,这样应用层只需求拜访下标值就能够了,不必去关心详细字断是什么,咱们现在给People类添加一个get函数

data class People(var nickName: String, var userId: Int,var age:Int=0) {
    operator fun get(index:Int):Any?{
        if(index == 0){
            return nickName
        }else if(index == 1){
            return userId
        }else{
            return age
        }
    }
    operator fun set(index:Int,value:Any?){
        if(index == 0){
            nickName = value as String
        }else if(index == 1){
            userId = value as Int
        }else{
            age = value as Int
        }
    }
}

咱们现在经过下标去拜访一个People类的特点,并经过下标去改变一个People的特点

val people = People("Tony",12,30)
println("name = ${people[0]},userid = ${people[1]},age = ${people[2]}")
......
name = Tony,userid = 12,age = 30
people[0] = "Peter"
people[1] = 20
people[2] = 23
println("name = ${people[0]},userid = ${people[1]},age = ${people[2]}")
......
name = Peter,userid = 20,age = 23

in与rangTo的约好

这两个约好我觉得放在一同说比较好,由于in表示判别是否在某一个区间里边,而rangeTo表示的是某一个区间,经常放在一同运用,比方咱们在用到for循环的时分,遍历1到10这十个数字并打印出来,咱们会这样做

for(i in 1..10){
    println(i)
}
......
1
2
3
4
5
6
7
8
9
10

除此之外,咱们还能够用in和rangeTo来判别某个日期在不在一个时刻段里边

val date = LocalDate.now()
val anotherDate = LocalDate.of(2022, 12, 12)
val startDate = LocalDate.of(2022, 12, 22)
val endDate = LocalDate.of(2023, 2, 22)
println("$date ${if (date in startDate..endDate) "在" else "不在"}规模里边")
println("$anotherDate ${if (anotherDate in startDate..endDate) "在" else "不在"}规模里边")
......
2023-01-29 在规模里边
2022-12-12 不在规模里边

解构声明

关于一些特点比较多的数据类,假如想要独自把这些特点拿出来放在一个变量里边,或许需求写好几行赋值句子,比方People类,现在要生成一个名字叫Tony,年纪30岁,用户ID是10的目标,然后再用三个变量保存People的三个特点,代码如下

val people = People("Tony",10,30)
val name = people.nickName
val userId = people.userId
val age = people.age

假如特点多一些,赋值句子就更多了,kotlin里边有更简练的作法

val (x,y,z) = People("Tony",10,30)
println("name = $x id = $y age = $z")
......
name = Tony id = 10 age = 30

一行代码就完结了生成目标以及给三个变量赋值的操作,这个在kotlin里边就叫做解构声明,将一个独自的有多个特点值的目标拆分开来,给多个变量进行赋值,这其间也用到了约好,像上面的代码其实编译器自动给People加上了componentN()的办法,N便是特点的方位,所以像上面的代码,编译器的眼里其实是这样的

val people = People("Tony",10,30)
val x = people.component1()
val y = people.component2()
val z = people.component3()
println("name = $x id = $y age = $z")

咱们也能够为非数据类手动加上componentN()办法,让它能够运用解构声明,比方现在有一个学生类

class Student(var name:String,var score:Int,var schoolName:String)

假如什么都不做,直接运用解构声明语法,编译器是会报错的

Kotlin系列之不知道约定,可能有些代码你看不懂
咱们现在给Student类加上componentN办法

class Student(var name:String,var score:Int,var schoolName:String) {
    operator fun component1() = name
    operator fun component2() = score
    operator fun component3() = schoolName
}

现在能够给Student类运用解构声明语法了

val (x,y,z) = Student("Li",90,"qinghuadaxue")
println("name = $x score = $y schoolName = $z")
......
name = Li score = 90 schoolName = qinghuadaxue

invoke约好

最终一个约好,也是我觉得在kotlin里边用处最广的一个约好,由于它与lambda表达式有关,咱们先从简单的开始说起,首先在kotlin里边规则了,假如一个类里边界说了一个invoke的约好函数,那么这个函数能够直接用类名替代invoke去调用,咱们仍是用代码直观的来看下

data class People(var nickName: String, var userId: Int=0,var age:Int=0) {
    operator fun invoke(address:String?){
        println("${nickName}住在$address")
    }
}

仍是在People里边,咱们界说了一个约好函数invoke,传入一个String,而且直接输入一段话,这个时分咱们在应用层除了能够直接用People目标调用invoke函数输入这段话以外,咱们还能够这样做

People("天才威")("狗熊岭")

直接把整个类名加上构造函数替代了invoke执行了整个函数,这种做法是不是很眼熟,咱们在调用一个lambda表达式的时分也是这么做的,咱们知道一个lambda表达式自身是不会执行的,假如想要执行一个lambda表达式里边的函数体,必需在后面加上一对(),现在咱们来看下这样做的原因

fun bear(message: () -> Unit) {
    message()
}

咱们先界说了一个高阶函数bear,函数接纳一个函数类型的参数message,message便是一个lambda表达式,咱们在bear函数体里边直接加上()调用message,到这儿,咱们反编译下这段代码,看下java代码

public final void bear(@NotNull Function0 message) {
   Intrinsics.checkNotNullParameter(message, "message");
   message.invoke();
}

熟悉高阶函数的都知道,一个非内联的高阶函数,它接纳的lambda表达式的参数其实便是一个匿名类,在java代码中,Function0便是匿名类,咱们去Function0里边看看

public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}

能够看到Function0便是一个接口,里边只要一个invoke函数,而且用operator操作符润饰,所以真实调用的时分咱们能够直接以Function0()这种形式调用,假如有参数,直接在()中加上参数即可

总结

约好的内容都讲完了,相信有部分约好其实在实践开发当中咱们现已在用了,有的咱们或许用的还很少,可是值得肯定的是,熟练掌握了约好,咱们写代码的功率,或许是代码自身的可读性方面,都会有很大的提升~