前言
大家好,好久不见。从Kotlin发布到现在已经有快十个年头了,从2016年发布正式版发展到现在已经有越来越多的开发者开始使用Kotlin开发项目,特别是安卓开发者,因为谷歌在2017年的 I/O 大会上正式宣布Kotlin正式成为安卓的一级开发语言,在2019年的 I/O大会上又宣布Kotlin为安卓的第一开发语言。。
后来看一些开发者论坛看大家学习Kotlin的越来越多,提到的好多东西竟然都看不懂,最过分的是已经使用kotlin写了一个项目了,Kotlin中的一些关键字都不知道是干啥用的,实在惭愧。
好了,下面开始知识点了,敲黑板划重点!
Kotlin中的标准函数
Kotlin中的标准函数指的是在 Standard.kt 中定义的函数,下面来写一下我认为经常使用的标准函数吧!
let
最开始决定使用Kotlin的时候的一个重要原因就是它把空指针异常提到了语言层面,但是这也是让很多像我一样的开发者头疼的地方。。
是,Kotlin为了空安全不允许定义为空的,想要定义的话就必须加上问号,但是。。。。很多情况就像下面的代码:
var num : String ?= null fun add (){ val Len1= num ?. Length val Len2= num !!. Length }
为了Kotlin的空安全必须使用问号点或者两个感叹号点来消除空安全的报错,只是为了消除报错,在Java中根本不需要的好嘛!要是只使用一次两次还好,咱们使用问号或者感叹号还好,但如果是一堆调用的呢?比如下面:
var zhu : ZhuJ ?= null fun test (){ zhu ?. name zhu ?. phone zhu ?. age zhu ?. sex }
参数多的时候怎么搞。。不说写的速度,烦都烦死了。。。。
后来。。。。发现根本没必要这样写啊!可以这样啊:
var zhu:ZhuJ? = null fun test() { zhu?.let { zhu-> zhu.name zhu.phone zhu.age zhu.sex } }
是不是瞬间感觉代码优雅了好多。。。全局变量都没问题,方法的实参更没问题了。。
知识点啊兄弟们,早知道这样就不写一堆问号和感叹号了。。。
with
这个标准函数的作用是Lambda中的代码会持有对象的上下文,其最后一行代码为返回值。
这么说不太好理解,来一段代码大家先看下吧:
operator fun String.times(n: Int): String { val sb = StringBuilder() repeat(n){ sb.append(this) } return sb.toString() }
这是一个String类的运算符重载的一个方法,意思很简单,就是重复字符串,参数为重复几次。大家可以发现,在这个方法中的 StringBuilder 对象被使用了好几回,没一会都需要写一次,但是。。。如果使用了with标准函数的话。。。。
operator fun String.times(n: Int): String { return with(StringBuilder()) { repeat(n) { append(this@times) } toString() } }
是不是感觉清爽了些许,很多情况可以这样来调用。
run
这个标准函数的作用其实和 with 基本一致,只是使用方法上有所不同,with 需要括号中写入对象来进行操作,run 则是对象点进行操作,上面代码使用 run 改写之后的代码如下:
operator fun String.times(n: Int): String { return StringBuilder().run { repeat(n) { append(this@times) } toString() } }
apply
这块要注意了,apply 使用方式和run一致,但是不同的是:最后一行不作为返回值,废话不多说,还拿上面代码改写:
operator fun String.times(n: Int): String { return StringBuilder().apply { repeat(n) { append(this@times) } }.toString() }
with 和 run 的最后一行都是返回值,而apply泽不是,这块一定要注意。
关键字
这块需要好好的总结下了,真的是,都写了一个项目了连使用语言的关键字都没认全。。一个一个来!
lateinit
这个关键字其实使用的很多,在定义全局变量为空的时候并不是非得用问号设置为可空的,如果你可以确定一定不为空可以使用 lateinit 这个关键字来定义全局变量,举个栗子:
lateinit var zhuJ: ZhuJ
当这样定义全局变量的时候就无需设置为可空了,比如安卓项目中的 adapter ,咱们肯定能确认会赋值,不会为空,那么就可以使用 lateinit 了。
这块需要注意的是,即使咱们觉得不会为空,但肯定会有特殊情况需要进行判断,需要进行判断的话要使用 isInitialized ,使用方法如下:
if (::zhuJ.isInitialized){ // 判断是否已经进行赋值 }
sealed
这个关键字之前一直没有进行使用,它用来修饰类,含义为密封类,之前一直没搞懂这个密封类有啥说啥用,这两天好好看了下,我理解的作用就是:可以使代码更加严密。
这样说感觉有点抽象,再举个栗子吧,平时咱们在封装一些工具的时候一般只会有成功和失败,咱们的做法一般是定义一个接口,然后再定义一个成功类和失败类来实现这个接口,最后再进行判断:
class Success(val msg: String) : Result class Fail(val error: Throwable) : Result fun getResult(result: Result) = when (result) { is Success -> result.msg is Fail -> result.error.message else -> throw IllegalArgumentException() }
上面代码都是咱们一般写的,虽然只有两种情况,但是必须再写 else 来进行判断,如果不写的话编译就过不了。但如果使用密封类的话就不会有这种情况出现:
sealed class Results class Success(val mag: String) : Results() class Failure(val error: Exception) : Results() fun getMessage(result: Results) { when (result) { is Success -> { println(result.mag) } is Failure -> { println(result.error.toString()) } } }
不仅不用再写else,而且在进行 when 判断时,kotlin 会检查条件是否包含了所有的子类,如果没有会提示你加上,这样就大大提高的代码的鲁棒性,也不会出现没有判断到的问题。
operator
这个关键字是运算符重载,其实在上面标准函数中已经使用到了,就是可以对运算符进行重新自定义,用来实现一些代码上不对劲但实际上对劲的需求,使用起来也很舒服。
这里来说一下咱们常用的运算符需要重载的函数吧:加号对应 plus、减号对应minus、乘号对应 times、除号对应 div、取余对应 rem、自增对应 inc、自减对应 dec。
具体使用方法就是上面那样,再写下吧:
operator fun String.times(n: Int): String {}
internal
这个关键字可以用来修饰类和方法,它的作用很简单,就是限制不同 module 的访问,如果在 A module 中定义了一个 internal 方法,那么这个方法只能在 A module 中进行调用,在 B module 中是无法访问的。
inner
这个关键字很简单,用来修饰类,但是。。。只能用来修饰内部类。
咱们来写个栗子大家就知道了:
class Test { var num: String? = null class Zhu() { var nums: String? = null fun adds() { nums?.let { it.length } } } inner class Jiang() { var nums: String? = null fun adds() { nums?.let { it.length } } } }
上面代码很简单,只是定义了一个 Test 类,其中一个是直接在内部创建的 Zhu 类,另一个是使用 inner 关键字修饰的 Jiang 类。咱们直接来调用下看下有什么区别吧:
Test().Jiang().nums Test.Zhu().nums
大家发现没有,如果要使用 inner 修饰内部类的话需要先获取到 Test 类的实例才可以进行使用,而直接创建的 Zhu 类则不需要。
inline
这个关键字的意思是内联函数,它的用法非常简单,只需要在高阶函数前加上 inline 关键字即可。如果对高阶函数不太清楚的,建议去看下扔物线的一个视频,好像是讲解 Lanbda 的。
简单说下吧,高阶函数并没有想象中的难,只是名字听着感觉很高大上而已,简单来说就是传入方法(其实本质上还是对象)当作方法参数即为高阶函数。高阶函数的原理其实就是把方法参数转为接口,并创建匿名内部类进行调用,所以每次调用这样的 Lambda 都会创建一个新的匿名内部类和接口实例,造成额外的开销。
所以这就是 inline 出现的原因,它可以去掉这些开销,并没有什么特殊的,只是进行替换,就是在你调用的地方把方法参数进行替换,从而减少内存和性能的开销。来看下使用方法吧:
inline fun high(block:(Int,Int) -> Int,block2:(Int,Int) -> Int){ block.invoke(5,6) block2.invoke(4,5) }
noinline
诶,这个关键字和上面内联函数的关键字好像是吧!这是因为如果一个高阶函数中有两个或以上的方法参数存在的话,如果使用 inline 关键字的话会把所有的方法参数都变为内联函数,为什么不都替换呢?因为内联函数的函数类型参数在编译的时候会进行代码替换,所以没有真正的参数类型,但非内联函数的函数类型参数可以自由的传递给其他任何函数,而内联函数类型参数只允许传递给另一个内联函数。
说了这么多就是为了引出 noinline 的存在意义,使用方法很简单:
inline fun high(block:(Int,Int) -> Int,noinline block2:(Int,Int) -> Int){ block.invoke(5,6) block2.invoke(4,5) }
是不是很简单,只要在方法参数前加上 noinline 关键字即可。
crossinline
既然已经说了 noinline 关键字,那么也得说下 crossnoinline 了。它的作用是:让无法使用内联函数的方法使用内联函数。
为什么有无法使用内联函数的函数呢?非内联函数无法直接 return ,但是内联函数可以,所以如果在高阶函数中创建或者使用了另外的 Lambda 或匿名类的实现的话即会报错。再举个栗子:
inline fun runAble ( block :()-> Unit ){ val a = Runnable { block . invoke () l } }
上面代码使用了咱们非常熟悉的 Runnable ,但是发现报错了,为什么呢?上面已经说出了答案,这就是 crossinline 关键字的作用,可以让无法使用内联函数的函数来使用内联函数:
inline fun runAble ( crossinline block :()-> Unit ) f val a = Runnable { block . invoke () } }
完美解决!crossinline 的作用主要是用于保证内联函数中的 Lambda 表达式中一定不会使用 return 关键字,这样也就没有冲突了。这样也有一个坏处,就是我们也无法调用 Runnable 中使用 return 进行返回了。
infix
这个关键字其实很好用,咱们可以使用它来一些很骚的操作:
val result = "zhujiang" * 3 val a = result begin "zhu"
是不是没见过这样的写法?复制到你电脑上肯定报错。infix 主要的作用就是定义一些语义上很舒服的写法,比如上面的 result begin “zhu” 这样的调用方式:
infix fun String.begin(prefix:String):Boolean = startsWith(prefix)
是不是很好用,是不是已经想到很多骚操作了?哈哈哈
但是!
要注意以下两点:
- infix 不能定义成顶层函数,必须是某个类的成员函数,可使用扩展方法的方式将它定义到某个类中
- infix 函数必须且只有能接收一个参数,类型的话没有限制。
by
这个关键字的意思是委托。来一个使用方法看看吧:
class MySet<T>(val help:HashSet<T>) :Set<T> by help{ override fun isEmpty(): Boolean { return false } }
可以为一些类创建委托类并重写或添加一些自己写的方法。
泛型
泛型大家再熟悉不过了,Java 中咱们使用的也非常多,例如 List 、HashMap<String,String>等等。
kotlin中的泛型
其实使用和 Java 中差不多,栗子又来了:
class Generic <T>{ fun method(parem:T):T{ return parem } }
上面是在类上的使用,当然方法中也可以进行使用:
fun <S> meth(parem:S):S{ return parem }
泛型的实化
在 Java 中是绝对没有的,也是不现实的,因为 Java 的泛型擦出机制。。。
但是在Kotlin中是可以实现的,但是。。。。有条件!
函数必须是内联函数,因为只有内联函数才有替换的操作。
声明类型时必须加上 reified 关键字来表示该泛型要进行实化。
那么,实化有什么作用呢?来看代码吧:
inline fun <reified T> startActivity(context:Context) { context.startActivity(Intent(context,T::class.java)) }
知道了吧。。很方便的!
泛型的逆变和协变
这块。。。说起来有点麻烦,下一篇文章来专门写写泛型的逆变和协变吧!先欠着!
总结
先总结到这里吧,其实 Kotlin 中还有很多好玩的东西需要我们去探索,比如协程,项目中其实用到了很多,但总感觉使用的不够好,需要有空好好扣一扣。