大多數人不知道的-Kotlin-技巧以及-原理解析

分享一篇很有用的乾貨,希望對大家的學習有所啓發和幫助~

Google 引入 Kotlin 的目的就是爲了讓 Android 開發更加方便,自從官宣 Kotlin 成爲了 Android 開發的首選語言之後,已經有越來越多的人開始使用 Kotlin。 結合着 Kotlin 的高級函數的特性可以讓代碼可讀性更強,更加簡潔,但是呢簡潔的背後是有代價的,使用不當對性能可能會有損耗,這塊往往很容易被我們忽略,這就需要我們去研究 kotlin 語法糖背後的魔法,當我們在開發的時候,選擇合適的語法糖,儘量避免這些錯誤。

通過這篇文章你將學習到以下內容,文中會給出相應的答案

  • 如何使用 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"
}

那麼我們如何在項目中使用呢,具體的用法可以查看我 GitHub 上的項目DataBindingDialog.kt當中的用法。

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

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) }

彙總

以表格的形式彙總,更方便去理解

使用 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,因爲初始化完成之後它的值是不會變的。

可接受參數的單例

但是有的時候,希望在單例實例化的時候傳遞參數,例如:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n107" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Singleton.getInstance(context).doSome()</pre>

上面這兩種形式都不能滿足,來看看大神 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

原作者:HiDhl 來源:掘金 鏈接:https://juejin.im/post/5edfd7c9e51d45789a7f206d

本文在開源項目:https://github.com/xieyuliang/Note-Android 中已收錄,裏面包含不同方向的自學編程路線、面試題集合/面經、及系列技術文章等,資源持續更新中...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章