为数很少的人知道的 Kotlin 技巧以及 原理解析

Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言以后,已经有愈来愈多的人开始使用 Kotlin。java

结合着 Kotlin 的高级函数的特性可让代码可读性更强,更加简洁,可是呢简洁的背后是有代价的,使用不当对性能可能会有损耗,这块每每很容易被咱们忽略,这就须要咱们去研究 kotlin 语法糖背后的魔法,当咱们在开发的时候,选择合适的语法糖,尽可能避免这些错误,关于 Kotlin 性能损失那些事,能够看一下我另外两篇文章。python

这两篇文章都分析了 Kotlin 使用不当对性能的影响,不只如此 Kotlin 当中还有不少让人傻傻分不清楚的语法糖例如 run, with, let, also, apply 等等,这篇文章将介绍一种简单的方法来区分它们以及如何选择使用。android

经过这篇文章你将学习到如下内容,文中会给出相应的答案c++

  • 如何使用 plus 操做符对集合进行操做?
  • 当获取 Map 值为空时,如何设置默认值?
  • require 或者 check 函数作什么用的?
  • 如何区分 run, with, let, also and apply 以及如何使用?
  • 如何巧妙的使用 in 和 when 关键字?
  • Kotlin 的单例有几种形式?
  • 为何 by lazy 声明的变量只能用 val?

plus 操做符

在 Java 中算术运算符只能用于基本数据类型,+ 运算符能够与 String 值一块儿使用,可是不能在集合中使用,在 Kotlin 中能够应用在任何类型,咱们来看一个例子,利用 plus (+) 和 minus (-) 对 Map 集合作运算,以下所示。git

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 的扩展函数。github

用 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"
}
复制代码

那么咱们如何在项目中使用呢,具体的用法能够查看我 GitHub 上的项目 DataBindingDialog.kt 当中的用法。

如何区分和使用 run, with, let, also, apply

感谢大神 Elye 的这篇文章提供的思路 Mastering Kotlin standard functions

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 在这篇文章给出的方法 Kotlin singletons with argument 代码以下。

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 等等能够设计出更加简洁并利于维护的代码,更多技巧能够查看我 GitHub 上的项目 JDataBinding

参考连接

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,请持续关注,除了翻译还有对每篇欧美文章思考,若是对你有帮助,请帮我点个赞,感谢!!!期待与你一块儿成长。

算法

因为 LeetCode 的题库庞大,每一个分类都能筛选出数百道题,因为每一个人的精力有限,不可能刷完全部题目,所以我按照经典类型题目去分类、和题目的难易程度去排序

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,而且每道题目都有解题思路,若是你同我同样喜欢算法、LeetCode,能够关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一块儿来学习,期待与你一块儿成长

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不只有助于分析问题,在面试过程当中,对咱们也是很是有帮助的,若是你同我同样喜欢研究 Android 源码,能够关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库

Android 应用系列

精选译文

工具系列

逆向系列

相关文章
相关标签/搜索