为数不多的人知道的 Kotlin 技巧及解析(二)
文章中没有奇淫技巧,都是一些在实际开发中常用技巧
Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。
结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是简洁的背后是有代价的,使用不当对性能可能会有损耗,这块往往很容易被我们忽略,这就需要我们去研究 kotlin 语法糖背后的魔法,当我们在开发的时候,选择合适的语法糖,尽量避免这些错误,关于 Kotlin 性能损失那些事,可以看一下我另外两篇文章。
[译][2.4K Start] 放弃 Dagger 拥抱 Koin
https://juejin.im/post/5ebc1eb8e51d454dcf45744e?utm_source=gold_browser_extension[译][5k+] Kotlin 的性能优化那些事
https://juejin.im/post/5ec0f3afe51d454db11f8a94#heading-7
这两篇文章都分析了 Kotlin 使用不当对性能的影响,不仅如此 Kotlin 当中还有很多让人傻傻分不清楚的语法糖例如 run, with, let, also, apply 等等,这篇文章将介绍一种简单的方法来区分它们以及如何选择使用。
通过这篇文章你将学习到以下内容,文中会给出相应的答案
如何使用 plus 操作符对集合进行操作? 当获取 Map 值为空时,如何设置默认值? require 或者 check 函数做什么用的? 如何区分 run, with, let, also and apply 以及如何使用? 如何巧妙的使用 in 和 when 关键字? Kotlin 的单例有那种三种写法? 为什么 by lazy 声明的变量只能用 val?
plus 操作符
在 Java 中算术运算符只能用于基本数据类型,+ 运算符可以与 String 值一起使用,但是不能在集合中使用,在 Kotlin 中可以应用在任何类型,我们来看一个例子,利用 plus (+) 和 minus (-) 对 Map 集合做运算,如下所示。
fun main() {
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)
// plus (+)
println(numbersMap + Pair("four", 4)) // {one=1, two=2, three=3, four=4}
println(numbersMap + Pair("one", 10)) // {one=10, two=2, three=3}
println(numbersMap + Pair("five", 5) + Pair("one", 11)) // {one=11, two=2, three=3, five=5}
// minus (-)
println(numbersMap - "one") // {two=2, three=3}
println(numbersMap - listOf("two", "four")) // {one=1, three=3}
}
其实这里用到了运算符重载,Kotlin 在 Maps.kt 文件里面,定义了一系列用关键字 operator 声明的 Map 的扩展函数。
用 operator 关键字声明 plus 函数,可以直接使用 + 号来做运算,使用 operator 修饰符声明 minus 函数,可以直接使用 - 号来做运算,其实我们也可以在自定义类里面实现 plus (+) 和 minus (-) 做运算。
data class Salary(var base: Int = 100){
override fun toString(): String = base.toString()
}
operator fun Salary.plus(other: Salary): Salary = Salary(base + other.base)
operator fun Salary.minus(other: Salary): Salary = Salary(base - other.base)
val s1 = Salary(10)
val s2 = Salary(20)
println(s1 + s2) // 30
println(s1 - s2) // -10
Map 集合的默认值
在 Map 集合中,可以使用 withDefault 设置一个默认值,当键不在 Map 集合中,通过 getValue 返回默认值。
val map = mapOf(
"java" to 1,
"kotlin" to 2,
"python" to 3
).withDefault { "?" }
println(map.getValue("java")) // 1
println(map.getValue("kotlin")) // 2
println(map.getValue("c++")) // ?
源码实现也非常简单,当返回值为 null 时,返回设置的默认值。
internal inline fun <K, V> Map<K, V>.getOrElseNullable(key: K, defaultValue: () -> V): V {
val value = get(key)
if (value == null && !containsKey(key)) {
return defaultValue()
} else {
@Suppress("UNCHECKED_CAST")
return value as V
}
}
但是这种写法和 plus 操作符在一起用,有一个 bug ,看一下下面这个例子。
val newMap = map + mapOf("python" to 3)
println(newMap.getValue("c++")) // 调用 getValue 时抛出异常,异常信息:Key c++ is missing in the map.
这段代码的意思就是,通过 plus(+) 操作符合并两个 map,返回一个新的 map, 但是忽略了默认值,所以看到上面的错误信息,我们在开发的时候需要注意这点。
使用 require 或者 check 函数作为条件检查
// 传统的做法
val age = -1;
if (age <= 0) {
throw IllegalArgumentException("age must not be negative")
}
// 使用 require 去检查
require(age > 0) { "age must be negative" }
// 使用 checkNotNull 检查
val name: String? = null
checkNotNull(name){
"name must not be null"
}
那么我们如何在项目中使用呢,具体的用法可以点击下方链接查看。
https://github.com/hi-dhl/JDataBinding/blob/master/jdatabinding/src/main/java/com/hi/dhl/jdatabinding/DataBindingDialog.kt
如何区分和使用 run, with, let, also, apply
感谢大神 Elye 提供的思路, run, with, let, also, apply 都是作用域函数,这些作用域函数如何使用,以及如何区分呢,我们将从以下三个方面来区分它们。
是否是扩展函数。 作用域函数的参数(this、it)。 作用域函数的返回值(最后一行、调用本身)。
是否是扩展函数
首先我们来看一下 with 和 T.run,这两个函数非常的相似,他们的区别在于 with 是个普通函数,T.run 是个扩展函数,来看一下下面的例子。
val name: String? = null
with(name){
val subName = name!!.substring(1,2)
}
// 使用之前可以检查它的可空性
name?.run { val subName = name.substring(1,2) }?:throw IllegalArgumentException("name must not be null")
在这个例子当中,name?.run 会更好一些,因为在使用之前可以检查它的可空性。
作用域函数的参数(this、it)
我们在来看一下 T.run 和 T.let,它们都是扩展函数,但是他们的参数不一样,T.run 的参数是 this, T.let 的参数是 it。
val name: String? = "hi-dhl.com"
// 参数是 this,可以省略不写
name?.run {
println("The length is ${this.length} this 是可以省略的 ${length}")
}
// 参数 it
name?.let {
println("The length is ${it.length}")
}
// 自定义参数名字
name?.let { str ->
println("The length is ${str.length}")
}
在上面的例子中看似 T.run 会更好,因为 this 可以省略,调用更加的简洁,但是 T.let 允许我们自定义参数名字,使可读性更强,如果倾向可读性可以选择 T.let。
作用域函数的返回值(最后一行、调用本身)
接下里我们来看一下 T.let 和 T.also 它们接受的参数都是 it, 但是它们的返回值是不同的,T.let 返回最后一行,T.also 返回调用本身。
var name = "hi-dhl"
// 返回调用本身
name = name.also {
val result = 1 * 1
"juejin"
}
println("name = ${name}") // name = hi-dhl
// 返回的最后一行
name = name.let {
val result = 1 * 1
"hi-dhl.com"
}
println("name = ${name}") // name = hi-dhl.com
从上面的例子来看 T.also 似乎没有什么意义,细想一下其实是非常有意义的,在使用之前可以进行自我操作,结合其他的函数,功能会更强大。
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
当然 T.also 还可以做其他事情,比如利用 T.also 在使用之前可以进行自我操作特点,可以实现一行代码交换两个变量,在后面会有详细介绍
T.apply 函数
通过上面三个方面,大致了解函数的行为,接下来看一下 T.apply 函数,T.apply 函数是一个扩展函数,返回值是它本身,并且接受的参数是 this。
// 普通方法
fun createInstance(args: Bundle) : MyFragment {
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
// 改进方法
fun createInstance(args: Bundle)
= MyFragment().apply { arguments = args }
// 普通方法
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data=Uri.parse(intentData)
return intent
}
// 改进方法,链式调用
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }
汇总
以表格的形式汇总,更方便去理解
函数 | 是否是扩展函数 | 函数参数(this、it) | 返回值(调用本身、最后一行) |
---|---|---|---|
with | 不是 | this | 最后一行 |
T.run | 是 | this | 最后一行 |
T.let | 是 | it | 最后一行 |
T.also | 是 | it | 调用本身 |
T.apply | 是 | this | 调用本身 |
使用 T.also 函数交换两个变量
接下来演示的是使用 T.also 函数,实现一行代码交换两个变量?我们先来回顾一下 Java 的做法。
int a = 1;
int b = 2;
// Java - 中间变量
int temp = a;
a = b;
b = temp;
System.out.println("a = "+a +" b = "+b); // a = 2 b = 1
// Java - 加减运算
a = a + b;
b = a - b;
a = a - b;
System.out.println("a = " + a + " b = " + b); // a = 2 b = 1
// Java - 位运算
a = a ^ b;
b = a ^ b;
a = a ^ b;
System.out.println("a = " + a + " b = " + b); // a = 2 b = 1
// Kotlin
a = b.also { b = a }
println("a = ${a} b = ${b}") // a = 2 b = 1
来一起分析 T.also 是如何做到的,其实这里用到了 T.also 函数的两个特点。
调用 T.also 函数返回的是调用者本身。 在使用之前可以进行自我操作。
也就是说 b.also { b = a } 会先将 a 的值 (1) 赋值给 b,此时 b 的值为 1,然后将 b 原始的值(2)赋值给 a,此时 a 的值为 2,实现交换两个变量的目的。
in 和 when 关键字
使用 in 和 when 关键字结合正则表达式,验证用户的输入,这是一个很酷的技巧。
// 使用扩展函数重写 contains 操作符
operator fun Regex.contains(text: CharSequence) : Boolean {
return this.containsMatchIn(text)
}
// 结合着 in 和 when 一起使用
when (input) {
in Regex("[0–9]") -> println("contains a number")
in Regex("[a-zA-Z]") -> println("contains a letter")
}
in 关键字其实是 contains 操作符的简写,它不是一个接口,也不是一个类型,仅仅是一个操作符,也就是说任意一个类只要重写了 contains 操作符,都可以使用 in 关键字,如果我们想要在自定义类型中检查一个值是否在列表中,只需要重写 contains() 方法即可,Collections 集合也重写了 contains 操作符。
val input = "kotlin"
when (input) {
in listOf("java", "kotlin") -> println("found ${input}")
in setOf("python", "c++") -> println("found ${input}")
else -> println(" not found ${input}")
}
Kotlin 的单例三种写法
我汇总了一下目前 Kotlin 单例总共有三种写法:
使用 Object 实现单例。 使用 by lazy 实现单例。 可接受参数的单例(来自大神 Christophe Beyls)。
使用 Object 实现单例
代码:
object WorkSingleton
Kotlin 当中 Object 关键字就是一个单例,比 Java 的一坨代码看起来舒服了很多,来看一下编译后的 Java 文件。
public final class WorkSingleton {
public static final WorkSingleton INSTANCE;
static {
WorkSingleton var0 = new WorkSingleton();
INSTANCE = var0;
}
}
通过 static 代码块实现的单例,优点:饿汉式且是线程安全的,缺点:类加载时就初始化,浪费内存。
使用 by lazy 实现单例
利用伴生对象 和 by lazy 也可以实现单例,代码如下所示。
class WorkSingleton private constructor() {
companion object {
// 方式一
val INSTANCE1 by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { WorkSingleton() }
// 方式二 默认就是 LazyThreadSafetyMode.SYNCHRONIZED,可以省略不写,如下所示
val INSTANCE2 by lazy { WorkSingleton() }
}
}
lazy 的延迟模式有三种:
上面代码所示 mode = LazyThreadSafetyMode.SYNCHRONIZED,lazy 默认的模式,可以省掉,这个模式的意思是:如果有多个线程访问,只有一条线程可以去初始化 lazy 对象。
当 mode = LazyThreadSafetyMode.PUBLICATION 表达的意思是:对于还没有被初始化的 lazy 对象,可以被不同的线程调用,如果 lazy 对象初始化完成,其他的线程使用的是初始化完成的值。
mode = LazyThreadSafetyMode.NONE 表达的意思是:只能在单线程下使用,不能在多线程下使用,不会有锁的限制,也就是说它不会有任何线程安全的保证以及相关的开销。
通过上面三种模式,这就可以理解为什么 by lazy 声明的变量只能用 val,因为初始化完成之后它的值是不会变的。
可接受参数的单例
但是有的时候,希望在单例实例化的时候传递参数,例如:
Singleton.getInstance(context).doSome()
上面这两种形式都不能满足,来看看大神 Christophe Beyls 给出的方案代码如下。
https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e
class WorkSingleton private constructor(context: Context) {
init {
// Init using context argument
}
companion object : SingletonHolder<WorkSingleton, Context>(::WorkSingleton)
}
open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile
private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}
有没有感觉这和 Java 中双重校验锁的机制很像,在 SingletonHolder 类中如果已经初始化了直接返回,如果没有初始化进入 synchronized 代码块创建对象,利用了 Kotlin 伴生对象提供的非常强大功能,它能够像其他任何对象一样从基类继承,从而实现了与静态继承相当的功能。所以我们将 SingletonHolder 作为单例类伴随对象的基类,以便在单例类上重用并公开 getInstance()函数。
参数传递给 SingletonHolder 构造函数的 creator,creator 是一个 lambda 表达式,将 WorkSingleton 传递给 SingletonHolder 类构造函数。
并且不限制传入参数的类型,凡是需要传递参数的单例模式,只需将单例类的伴生对象继承于 SingletonHolder,然后传入当前的单例类和参数类型即可,例如:
class FileSingleton private constructor(path: String) {
companion object : SingletonHolder<FileSingleton, String>(::FileSingleton)
}
总结
到这里就结束了,Kotlin 的强大不止于此,后面还会分享更多的技巧,在 Kotlin 的道路上还有很多实用的技巧等着我们一起来探索。
例如利用 Kotlin 的 inline、reified、DSL 等等语法, 结合着 DataBinding、LiveData 等等可以设计出更加简洁并利于维护的代码,更多技巧在项目中的使用,可以点击下方链接前去查看。
https://github.com/hi-dhl/JDataBinding
参考链接:
https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84
https://proandroiddev.com/kotlin-fun-with-in-8a425704b635
推荐阅读:
再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度 再见 SharedPreferences 拥抱 Jetpack DataStore Kotlin StateFlow 搜索功能的实践 DB + NetWork
最后推荐我一直在更新维护的项目和网站:
最新的 AndroidX Jetpack 相关组件的实战项目 以及 原理分析的文章
https://github.com/hi-dhl/AndroidX-Jetpack-PracticeLeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析
剑指 offer:https://offer.hi-dhl.com
LeetCode:https://leetcode.hi-dhl.com最新 Android 10 源码分析系列文章
https://github.com/hi-dhl/Leetcode-Solutions-with-Java-And-Kotlin一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的分析
https://github.com/hi-dhl/Technical-Article-Translation「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址
https://site.51git.cn
致力于分享一系列最新技术原创文章
长按二维码即可关注
我知道你在看哟