爲數很少的人知道的 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 應用系列

精選譯文

工具系列

逆向系列

相關文章
相關標籤/搜索