首页 > 其他分享 >Kotlin系列之不知道约定,可能有些代码你看不懂

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

时间:2023-06-19 12:36:09浏览次数:38  
标签:看不懂 val People Kotlin 代码 运算符 println LocalDate 函数


约定是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系列之不知道约定,可能有些代码你看不懂_编译器_02

Kotlin系列之不知道约定,可能有些代码你看不懂_开发语言_03

而我们可以利用这种约定,用在其他场景上,比如一个接口的数据类有若干个字断,分别运用在业务场景的各个角落,而如果有一天服务端同学告诉你要更改某一个字断,我们是不是要连着去改所有业务场景中用过这个字断的地方,有的字断用的少还好,有的字断用的地方多可能一改就要改半天,这个时候我们可以尝试着在数据类里面约定个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系列之不知道约定,可能有些代码你看不懂_运算符_04

我们现在给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()这种形式调用,如果有参数,直接在()中加上参数即可

总结

约定的内容都讲完了,相信有部分约定其实在实际开发当中我们已经在用了,有的我们可能用的还很少,但是值得肯定的是,熟练掌握了约定,我们写代码的效率,或者是代码本身的可读性方面,都会有很大的提升~

作者:Coffeeee

标签:看不懂,val,People,Kotlin,代码,运算符,println,LocalDate,函数
From: https://blog.51cto.com/u_16163453/6512372

相关文章

  • FlutterUnit 工具集录 | IconFont 类代码自动生成
    1.IconFont类代码生成器的作用首先介绍一下FlutterUnit中,代码生成菜单下的IconFont工具的作用。它主要解决Flutter项目中自定义字体图标使用的问题:字体图标调用类代码的自动生成。pubspec.yaml中字体图标节点的自动配置。多个自定义字体图标节点的支持。一键自动生成相......
  • kotlin 和 r8 的量子纠缠 | 类加载机制偷鸡
    前言戏接上文,kotlin升级没想到啊还有一个大坑。我们之前说了我们使用的agp版本是7.0.3,在这个版本的R8竟然会出现kotlin混淆的bug。断更一个月,不更文的一个原因就是因为最近感觉太菜了,并没有文章素材了。问题排查接下来还是一点点进行问题分析,我们先从kotlin元数据开始讲这个问题。......
  • 代码如何连接数据库?常用的方式有几种?
    在现代应用程序开发中,与数据库的交互是非常重要的一步。要想使用数据库,我们需要对应的编程语言提供的库或框架来实现数据库的连接和交互。本文将从几个方面介绍代码如何连接数据库,并详细介绍常用的方式。一、什么是数据库连接?数据库连接是指应用程序通过特定的接口与一个或多个数据......
  • 使用Kotlin+Rretrofit+rxjava+设计模式+MVP封装网络请求
    0、前言:kotlin使用起来非常顺畅,尤其是结合rxjava、设计模式使用,你会发现写代码原来可以这么开心!什么?你还在使用java?赶紧去学一下kotlin吧!我相信你一定会对他爱不释手,kotlin也很容易学,花一天的时间就可以从java切换为kotlin一、正文本文主要介绍如何使用kotlin封装网络请求的工具,结......
  • Kotlin版本的WanAndroid项目实战(三):Kotlin的集合
    集合概述学习方法:扫描一遍下述表格中集合相关的有哪些操作,具体使用时再可以详细去查构造集合Kotlin里面添加了可变集合的概念,目前只是一种接口级别的限制,底层实现还是可变的集合,不是线程安全的,还是可以通过Java代码修改内部元素迭代器(1)Iterable接口的继承者(包括Set与......
  • Kotlin与Java互调原理项目实战
    数天前我将我java开发的工程,全部转换成了kotlin形式的工程。如果你也想做,本身也有一定的java开发安卓程序的功底。本文将比较适合你。创建kotlin工程,拷贝类文件xml文件等核心文件到工程目录下,形成一个kotlin底子的java代码组成的工程,然后通过ctrl+shift+alt+k快捷代码逐个转换......
  • kotlin协程原理分析项目实战
    自从6.0加入动态权限之后,很多地方都要用到,开始的时候使用的原生代码写权限请求,代码格式如:if(应用是否已经请求了该权限组){权限请求后的代码}else{请求权限}然后不知道在fragment或是activity里面重写overrideonRequestPermissionsResult{if(请求成功){......
  • ASP.NET Core 6框架揭秘实例演示[39]:使用最简洁的代码实现登录、认证和注销
    认证是一个确定请求访问者真实身份的过程,与认证相关的还有其他两个基本操作——登录和注销。ASP.NETCore利用AuthenticationMiddleware中间件完成针对请求的认证,并提供了用于登录、注销以及“质询”的API,本篇文章利用它们使用最简单的代码实现这些功能。(本文提供的示例演示已经同......
  • 用kotlin来开发一个cli工具 | 没用的技能+1
    脚手架脚手架是为了保证各施工过程顺利进行而搭设的工作平台而在程序开发过程中,每个工程或者说公司也都需要一个脚手架工具。通过脚手架命令行的形式简化开发流程,避免发生一些人为的相对低级的问题,所以这个也就是为什么叫做脚手架的原因吧。而由于每个公司的代码规范都不同,一般情况......
  • Android代码检查规则Lint的自定义与应用
    前言:在日常的代码开发中,此处相信每个开发人员对代码质量都是高要求,有自己的一套代码规范,但是我们不是单独作战,往往大家都是团队作战,人是最大的变量,各人各异,如何保证团队的代码质量和代码规范呢?靠开发者自觉吗?也许有的团队有严格的CR机制,在MR阶段会进行CR,CR不通过的MR是不允许合入的......