重学Kotlin之那些你没注意到的细节

简介: Kotlin中的一些关键字

前言

大家好,好久不见。从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 中还有很多好玩的东西需要我们去探索,比如协程,项目中其实用到了很多,但总感觉使用的不够好,需要有空好好扣一扣。


目录
相关文章
|
4月前
|
Android开发 Kotlin
android开发,使用kotlin学习WorkManager
android开发,使用kotlin学习WorkManager
90 0
|
4月前
|
Android开发 Kotlin
android开发,使用kotlin学习Lifecycles
android开发,使用kotlin学习Lifecycles
72 0
|
XML 存储 算法
Kotlin 实战 | 时隔一年,用 Kotlin 重构一个自定义控件
一年前,用 Java 写了一个高可扩展选择按钮库。只用单个控件实现单选、多选、菜单选,且选择模式可动态扩展。 一年后,试着用 Kotlin 重写该控件。
782 0
|
XML 存储 缓存
怎么用Kotlin去提高生产力:Kotlin Tips
汇总Kotlin相对于Java的优势,以及怎么用Kotlin去简洁、务实、高效、安全的开发,每个小点tip都有详细的说明和案例代码,争取把每个tip分析得清楚易懂,会不断的更新维护tips,欢迎fork进来加入我们一起来维护,有问题的话欢迎提Issues。 • 推荐一个Kotlin的实践项目debug_view_kotlin,用kotlin实现的Android浮层调试控制台,实时的显示内存、FPS、文字log
206 1
|
消息中间件 算法 前端开发
Kotlin可能带来的一个深坑,实战篇
Kotlin可能带来的一个深坑,实战篇
Kotlin可能带来的一个深坑,实战篇
|
Java 编译器 Kotlin
刨下Kotlin | 9. @JvmOverloads 原理 & 一个小细节
刨下Kotlin | 9. @JvmOverloads 原理 & 一个小细节
211 0
|
安全 Java Kotlin
Kotlin刨根问底(一):你真的了解Kotlin中的空安全吗?(上)
行文结构:要点提炼(方便回顾) + 常规操作 + 源码层面摸索实现原理, 内容部分摘取自:《Kotlin实用指南》
201 0
|
安全 Java 编译器
Kotlin刨根问底(一):你真的了解Kotlin中的空安全吗?(下)
行文结构:要点提炼(方便回顾) + 常规操作 + 源码层面摸索实现原理, 内容部分摘取自:《Kotlin实用指南》
126 0
|
Java 调度 Kotlin
Kotlin的扩展函数知识点
Kotlin的扩展函数知识点
160 0
|
Web App开发 Java Android开发