Kotlin 是一门新兴的编程语言,它具备空安全,函数式编程等许多 Java 所不具备的新特点,许多同事对它的一些细节还没有那么熟悉,本手册旨在帮助大家了解一些在学习中不易被记忆的 Kotlin 的使用细节;其中包括一些类库中定义的许多方便的扩展函数,或者一些更为优雅的语法细节;使用本手册中介绍的 Kotlin 技巧,可以帮助您的代码更优雅,更具有复用性,甚至可以做到零警告,我们,都知道,编译器给出的警告虽然不会影响代码的运行,但是也代表了一种潜在的危险,因此,消除警告就非常有意义;我们将通过以下几点,来详细展开本手册的内容:
- 一. 空安全
- 二. 延时初始化的最优写法
- 三. 字符串操作
- 四. 集合操作相关的扩展函数
- 五. 多线程
- 六. 其它
一. 空安全
在 Kotlin 中,我们应尽量将变量声明成非空的,这样做有利于最大程度的杜绝空指针异常,但有时,我们会遇到必须将变量声明称可空类型的情况,面对这种情况,Kotlin也提供了多种语法来帮助我们进行可空调用,这里相信大家都理解,所以只做一个简短回顾。
非空判断
class Person(val name: String? = "", val age: Int = 0) {
fun doSomething() {
println("do something")
}
}
fun function(person: Person?) {
if (person != null) {
println(person.age)
}
}
非空调用
fun function(person: Person?) {
person?.doSomething()
}
Elvis运算符
fun function(person: Person?) {
println(person.age ?: 23)
}
let函数
fun function(person: Person?) {
person?.let {
println(it.age)
println(it.name ?: "Mark")
}
}
非空断言
fun function(person: Person?) {
person!!.domeSomething()
}
这几种可空调用语法分别对应不同的情况,这里直接说使用它们的原则:
1.如果在一段连续的代码中要使用可空对象的属性,且只使用一次,最优写法是 Elvis 运算符。
2.如果在一段连续的代码中要调用可空对象的方法,且只调用一次,请使用非空调用。
3.如果在一段连续的代码中,要对一个可空对象进行多次非空操作(包括使用属性,或调用函数),请使用 let 函数。
4.如果在一段连续的代码中,要对一个可空对象进行多次非空操作,且还要用较为复杂的逻辑处理空情况的时候,请使用非空判断。
5.不到万不得已,不要用非空断言。
以上这些 tips 的结论来自于两点,首先是判空次数,例如 3 和 4 的情况,如果要连续进行多次非空调用,实际上只需要进行一次判空就可以了,这样可以节省判空带来的开销,如果在 3 和 4 的情况拼房使用非空调用,将是一种性能糟糕的写法。其次,代码的优雅程度,我们应该尽可能的减少代码的复杂度和潜逃层级(即大括号嵌套的情况),使代码更易阅读,Elvis 运算符和非空调用都是这样的的语法糖,因此,在非空调用和Elvis运算符能够胜任的简单情况下,我们无需使用非空判断和let函数。最后一点,非空断言是非常危险的一种写法,在我的编程经验中,通常有两种情况才会用到,其一是自己手动实现某种数据结构时,例如用 kotlin 做算法题的时候经常会用到,其二是当一个可空对象被赋值后立即使用它时,我们明明已经给它赋值过,但是编译器显然无法理解这一点,在编译器看来,可空类型就是另外一种类型,如果不进行手动强制转型或者经过非空判断的智能转换,它都不可以直接调用成员函数,但这时在我们极度确信的情况下,我们才能够使用非空断言。
二. 延时初始化的最优写法
我们有时为了加快启动速度,只需要让对象在它第一次被调用的时候进行初始化,或者我们在声明一个对象时,由于缺少某些必要的参数,无法立即进行初始化,这时我们需要使用延时初始化功能。
by lazy
如果我们需要将一个对象延时初始化,我们首先应该使用的是 by lazy 写法,如下所示:
private val mRed by lazy { ContextCompat.getColor(mContext, R.color.red) }
有时候我们需要根据不同的情况给一个 TextView 设置为不同的颜色,但我们无需在这个界面(可能是 Activity 也可能是 Fragment 或一个 RecyclerView的Adapter)初始化的时候就把所有需要的颜色都初始化给一个成员引用,而是在第一次用到这个颜色的时候再初始化它,这样可以节省界面打开的耗时,也能在一定程度上节约内存(例如在初始化一个更大的对象的时候更为明显)。
lateinit
在另外一些情况下,我们的某个对象的初始化需要一些在声明时无法获取到的参数,这时候 by lazy 无法胜任这样的工作,而 lateinit 在这时就是最优的写法,其中一个很重要的原因就是 lateinit 属性在调用自己的方法或使用自己的成员变量的时候无需进行可空调用。
class MyPresenter() : Presenter {
private lateinit var mView: View
override fun init(baseView: View) {
mView = baseView
}
}
以上这种情形,在当前 OKEx 的 Android 客户端中经常会见到,是我们在使用MVP模式时最常见的 Presenter 的例子。我曾见过一些早期代码,采用如下方式进行延时初始化:
class MyPresenter() : Presenter {
private var mView: View? = null
override fun init(baseView: View) {
mView = baseView
}
}
看上去变化不大,就是将 lateinit 换成了可空类型;这样写的弊端在于,之后在每次 mView 调用方法的时候都要进行可空调用,上一节中已经说了,可空调用的本质就是在调用前进行判空操作,在 MVP 这样的样例中,这样的操作显然是多余的,因为我们可以确保 mView 只要在 inti() 函数中赋值就一定是非空的。
换一种思路想一下,如果我们的lateinit属性没有赋值,我们就使用它调用它自己的成员函数,会发生什么情况?会报错,而程序会 crash。但是在某些特殊情况下,我们确实不能确保 lateinit 属性已经被初始化了,这时我们可以使用如下的语法对 lateinit 属性进行检查:
if (thia::mView.isInitialized) {
// 还未初始化
} else {
// 已经初始化
}
通过成员引用的方式可以获取到对象的 isInitialized 属性,如果它为 true,则表示还未初始化,否则就是已经初始化;如果你的 lateinit 对象常常使你不能确定你在使用它时是否它已经被初始化,即如果每次使用它都要进行判断的话,建议还是使用可空类型,这样会更优。
三. 字符串操作
Kotlin 的字符串和 Java 的字符串大体上没有区别,但是 Kotlin 增加了许多扩展函数,字符串操作进行了大量的语法优化,本节主要介绍一些常用且典型的扩展库函数。
字符串空判断:
我们常常需要对字符串进行空判断,我们在使用 Java 时常使用 TextUtils 类的 isEmpty() 方法,但是在 Kotlin 中我们有更优秀的写法:
val str1 = "123"
str1.isEmpty() // false
str1.isBlank() // false
str1.isNotEmpty() // true
str1.isNotBlank() // true
val str2 = ""
str2.isEmpty() // true
str2.isBlank() // true
str2.isNotEmpty() // false
str2.isNotBlank() // false
val str3 = " "
str3.isEmpty() // false
str3.isBlank() // true
str3.isNotEmpty() // true
str3.isNotBlank() // false
如上面的例子所示,一共有四个函数:isEmpty(),isBlank(),isNotEmpty(),isNotBlank()。其中 isEmpty() 与 isNotEmpty() 是互逆的,而 isBlank() 与 isNotBlank() 也是互逆的。之所以要有这样互逆的两个函数是因为它们可以在我们写程序时语意表达更清晰,而不必使用“!”这样的取反操作符来降低代码的可读性。
isEmpty()和isBlank()的区别在于,如果一串字符串只含有空格,isEmpty()会返回false,而isBlank()会返回true。
charAt() 与 get()
在 Java 中,我们可以使用 charAt(int index) 方法来获取字符串指定位置的 char 字符,而在 Kotlin 中,这个方法被隐藏了,取而代之的是 get(index: Int);这是一个小细节,没什么值得多说的,具体情况看下面的例子:
//Java
String str = "Android";
char c = str.charAt(2); //c = ‘d’
//Kotlin
val str = "Android"
char c = str.get(2) //c = 'd'
in 操作符在字符串中的重载
如果我们要判断一个字符串中是否包含某个子字符串或某个子字符,我们可以使用 in 操作符,具体如以下示例:
val str = "Kotlin"
val b1 = 't' in str //true
val b2 = "lin" in str //true
字符串模版
字符串模版是一种可读性更高的语法,它优于传统的"+"操作符。如下所示:
val str1 = 'I'
val str2 = "love"
// 推荐写法,输出:I love Kotlin
println("$str1 $str2 Kotlin")
// 不推荐的“+”写法,输出:I love Kotlin
println(str1 + " " + str2 + " " + "Kotlin")
四. 集合相关的扩展函数
集合判空
val list = ArrayList<String>()
// Java 时代的旧写法
if (list.size == 0) // 集合为空
if (list.size != 0) // 集合不为空
// Kotlin 提供的扩展函数
if (list.isEmpty()) // 集合为空
if (list.isNotEmpty()) // 集合不为空
我们可以使用 isEmpty() 和 isNotEmpty() 来判断这个集合容器中是否已经装入元素。
遍历
val list = ArrayList<String>()
list.forEach {
//最常见的遍历集合的元素
}
list.forEachIndexed { index, str ->
// 带标号的遍历,index是指当前元素在集合中的位置,而str则是元素本身的引用
}
讲解请看样例代码的注释。
在少数情况下,我们还是需要使用 for 循环来遍历集合,我们通常会这样写:
for (i in 0 until list.size) {
//do something......
}
但是 until 和".."操作符我们有时经常会用错,所幸 Kotlin 的集合有更优的写法:
for (i in list.indices) {
//do something......
}
成员indices直接表示集合的区间。
其它大量流操作函数
在《Kotlin 实战》的第五章的 5.2 小节中介绍了大量的集合的流操作函数,它们支持链式调用,其中包括:filter(过滤),map(转换),all(是否都满足某条件),find(查找)等等,使用它们可以对集合进行一些强大且复杂的操作,且这些函数的算法都是优化过的,这一章的后续内容还介绍了一些更为复杂的函数,以及将集合转化为序列,使得内存空间利用率更高等,具体内容请参照相关章节。
在这里我介绍一些书中没有提到的,但是非常强大的函数。
val list = ArrayList<Person>()
// 将集合中包含的元素的某一属性,全部相加求和
val age = list.sumBy { it.age } // 得到所有人的总年龄
// 将集合中以某一条件判断的,最大/最小的元素,装入数组并返回
val maxAge = list.maxBy { it.age }
val maxAge = list.minBy { it.age }
这里再对 maxBy 和 minBy 两个函数多说两句,例如集合中有三个人,a,b,c;且a.age = 22,b.age = 23,c.age = 23。调用 maxBy 后会返回一个 Array<Person> 数组,数组中包含 b 和 c,调用 minBy 后也会返回一个数组,数组中只包含 a。即这两个函数会找到最大值/最小值,然后把所有拥有最大值/最小值的元素都装入数组并返回。
五. 多线程
在编写多线程程序时,我们在使用 Java 的时候常常会使用一些关键字,例如 synchronized 用来声明方法或代码块是同步的,volatile 关键字用来表示变量是可见性的;但是 Kotlin 中没有了这两个关键词,取而代之,我们可以使用注解 @Synchronized 与 @Volatile 来表示同步方法和可见性属性。
如果我们要对一个代码块添加同步锁,我们可以使用如下库函数:
// 对应Java中synchronized关键字修饰的同步代码块
synchronized(Any) {
// 同步代码块
}
除此之外的一些使用类库完成的同步操作,例如可重入锁 ReentrantLock 等不受影响。
在 Java 中我们常常使用 try-finally 语法来添加使用 ReentrantLock,而在 Kotlin 中我们使用 tryLock 扩展函数:
// Java 中使用 RenntrantLock
Lock lock = new ReentrantLock();
try {
lock.lock(); // 上锁
/**
* 同步代码
*/
} finally {
lock.unLock(); // 解锁
}
// Kotlin 中使用 RenntrantLock
val lock = ReentrantLock()
lock.tryLock {
/**
* 同步代码
*/
}
try-finally 语法不优雅不说,还需要我们手动的上锁和解锁,而 tryLock 扩展函数在内部封装了上锁与解锁的动作,更优雅方便以及准确,我们在编码时,如果设计使用可重入锁,我们应该使用 tryLock 扩展函数。
注意:本节内容只是针对兼容老代码的情况,在协程引入项目后,我们应该使用协程来进行异步和并发,尽量避免直接使用线程;使用协程的目的是避免进程内存在大量阻塞状态的线程消耗系统资源以及协程可以彻底消灭在线程层面上的死锁情况出现。
更多细节,请参阅协程相关的资料。
六. 其它
解构与表达式
解构在简化语法上非常有用:
// 遍历 Map
for ((key, value) in mHashMap) {
// do something
}
当一个表达式或者函数要返回两个或两个以上的值的时候,解构也非常优雅:
val (color, backgrond) = if (isRed) {
mRed to mRedBackground
} else {
mGreen to mGreenBackground
}
IO 流操作函数——use
在 Java 7 之前(虽然在 Java 7 之后也有很多人这么写)我们常使用 try-finally 语法来使用 IO 流,这和上面可重入锁的问题一样——需要我们显式关闭流,在 Java 7 之后,Java 提供了 try-with-resource 语法来优化这个问题。
但在 Kotlin 中没有 try-with-resource 语法,取而代之的是 use 扩展函数:
// Java 7 之前
BufferReader bufferReader = new BufferedReader(new InputStreamReader(context.assets.open(fileName), "UTF-8"));
try {
// IO 流操作
bufferReader.readLine();
} finally {
bufferReader.close();
}
// Java 7 以及之后的版本
try (BufferReader bufferReader = new BufferedReader(new InputStreamReader(context.assets.open(fileName), "UTF-8"))) {
// IO 流操作
bufferReader.readLine();
}
// Kotlin
BufferedReader(InputStreamReader(context.assets.open(fileName), "UTF-8")).use {
// IO 流操作
it.readLine()
}
apply 与 with 函数
当我们频繁调用某一个对象的属性或者方法的时候,最大的不优雅之处就在于我们要将这个对象写无数遍(这在编写 RecyclerView 的 Adapter 的 onBindViewHolder 方法的代码时尤为常见),我们可以通过 apply 或者 with 函数来消除这种样板代码,其中的原理是——带接收者的 lambda:
// 不使用任何优化
holder.mTitle.text = "123456"
holder.mContent.text = "123456"
holder.mTime.text = "123456"
holder.mImageView.setBitmap(data.bitmap)
// 使用 with 函数
with(holder) {
mTitle.text = "123456"
mContent.text = "123456"
mTime.text = "123456"
mImageView.setBitmap(data.bitmap)
}
// 使用 apply 函数
holder.apply {
mTitle.text = "123456"
mContent.text = "123456"
mTime.text = "123456"
mImageView.setBitmap(data.bitmap)
}
我们可以看到,使用这两个函数可以将每一行代码原本要写的 holder 全部省略掉;with 函数与 apply 的区别除了例子中能看出来的——with 是顶层函数,而 apply 是扩展函数外,另一个区别就在于 apply 函数返回调用它的对象,而 with 函数不返回任何东西。
除此之外,apply 函数的用途有时候可以类似于 上面空安全中所讲的 let 函数,假如上面例子中的 holder 是可空类型,那么使用 let 和 apply 就是下面这样:
// let 函数
holder?.let {
it.mTitle.text = "123456"
it.mContent.text = "123456"
it.mTime.text = "123456"
it.mImageView.setBitmap(data.bitmap)
}
// 使用 apply 函数
holder?.apply {
mTitle.text = "123456"
mContent.text = "123456"
mTime.text = "123456"
mImageView.setBitmap(data.bitmap)
}
可以看到,在这种情况下 apply 比 let 更优雅,所以 apply 函数在空安全中也大有可为,和 let 函数相比应该用谁,要视具体情况。
网友评论