/ 今日科技快讯 /
近日,据国外媒体报道,美国电动汽车制造商特斯拉股票收于1079.81美元,上涨6.98%,创下历史新高。这也使得特斯拉市值突破2000亿美元,稳坐全球市值最高汽车制造商的头把交椅。自2010年6月29日上市以来,特斯拉股票十年间上涨逾40倍。
/ 作者简介 /
本篇文章来自Zhujiang的投稿,分享了Kotlin的许多知识点,以及需要注意的一些内容,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!
Zhujiang的博客地址:
https://juejin.im/user/5c07e51de51d451de84324d5
/ 前言 /
大家好,好久不见。从Kotlin发布到现在已经有快十个年头了,从2016年发布正式版发展到现在已经有越来越多的开发者开始使用Kotlin开发项目,特别是安卓开发者,因为谷歌在2017年的 I/O 大会上正式宣布Kotlin正式成为安卓的一级开发语言,在2019年的 I/O大会上又宣布Kotlin为安卓的第一开发语言。。
我从2018年下半年开始学习的Kotlin,最开始买了一本《Kotlin从零到精通》,跟着学习了一遍,把Kotlin的基本语法学习了一遍,由于待的公司比较小,安卓开发者并不多,而且当时正好有一个新的项目,所以我提议整个项目采用Kotlin进行开发,其实当时很冒险,因为那个项目挺着急,三个安卓开发都不咋会Kotlin,所以在项目开发期间一直是边学边写,项目完成之后感觉自己对Kotlin已经理解的差不多了(其实只是会用而已,项目中有的地方还是使用Java)。
后来看一些开发者论坛看大家学习Kotlin的越来越多,提到的好多东西竟然都看不懂,最过分的是已经使用kotlin写了一个项目了,Kotlin中的一些关键字都不知道是干啥用的,实在惭愧。
之前一直关注郭神的博客,而且安卓入门也看的是郭神的《第二行代码》。后来听说要出《第三行代码》,而且里面有Kotlin的详细讲解,所以书第一天发售的时候就立马抢了一本签名版,还没来的及好好看看,公司就让我出差了,一直到现在才有空看看,看了看感觉之前写的代码好多都不太好,好多Kotlin中好用的东西都没使用到,我使用的Kotlin只是把Java代码转成了Kotlin,没有一点意义。好了,下面开始知识点了,敲黑板划重点!
/ Kotlin中的标准函数 /
Kotlin中的标准函数指的是在 Standard.kt 中定义的函数,下面来写一下我认为经常使用的标准函数吧!
let
最开始决定使用Kotlin的时候的一个重要原因就是它把空指针异常提到了语言层面,但是这也是让很多像我一样的开发者头疼的地方。。
是,Kotlin为了空安全不允许定义为空的,想要定义的话就必须加上问号,但是。。。。很多情况就像下面的代码:
为了Kotlin的空安全必须使用问号点或者两个感叹号点来消除空安全的报错,只是为了消除报错,在Java中根本不需要的好嘛!要是只使用一次两次还好,咱们使用问号或者感叹号还好,但如果是一堆调用的呢?比如下面:
参数多的时候怎么搞。。不说写的速度,烦都烦死了。。。。后来。。。。发现根本没必要这样写啊!可以这样啊:
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 或匿名类的实现的话即会报错。再举个栗子:
上面代码使用了咱们非常熟悉的 Runnable ,但是发现报错了,为什么呢?上面已经说出了答案,这就是 crossinline 关键字的作用,可以让无法使用内联函数的函数来使用内联函数:
完美解决!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(val help:HashSet) :Set by help{
override fun isEmpty(): Boolean {
return false
}
}
可以为一些类创建委托类并重写或添加一些自己写的方法。
/ 泛型 /
泛型大家再熟悉不过了,Java 中咱们使用的也非常多,例如 List 、HashMap
kotlin中的泛型
其实使用和 Java 中差不多,栗子又来了:
class Generic {
fun method(parem:T):T{
return parem
}
}
上面是在类上的使用,当然方法中也可以进行使用:
fun meth(parem:S):S{
return parem
}
泛型的实化
在 Java 中是绝对没有的,也是不现实的,因为 Java 的泛型擦出机制。。。
但是在Kotlin中是可以实现的,但是。。。。有条件!
函数必须是内联函数,因为只有内联函数才有替换的操作。
声明类型时必须加上 reified 关键字来表示该泛型要进行实化。
那么,实化有什么作用呢?来看代码吧:
inline fun startActivity(context:Context) {
context.startActivity(Intent(context, T::class.java))
}
知道了吧。。很方便的!
泛型的逆变和协变
这块。。。说起来有点麻烦,下一篇文章来专门写写泛型的逆变和协变吧!先欠着!
/ 总结 /
先总结到这里吧,其实 Kotlin 中还有很多好玩的东西需要我们去探索,比如协程,项目中其实用到了很多,但总感觉使用的不够好,需要有空好好扣一扣。虽然说内容并不多,但也写了好久,文中的知识都是跟着郭神的《第三行代码》学习的,也推荐大家购买,一本书能学到一个知识点就不亏,支持正版,别为了省几十块钱下载盗版。。。
推荐阅读:
我的新书,《第一行代码 第3版》已出版!
手撸一个计算器,乐趣还真不少~
我新开发了一个特别好用的开源库
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注