一些 Kotlin 小技巧及解析

2021-03-02 鴻洋

連結:

https://juejin.im/post/5edfd7c9e51d45789a7f206d

本文由作者授權發布。

Google 引入 Kotlin 的目的就是為了讓 Android 開發更加方便,自從官宣 Kotlin 成為了 Android 開發的首選語言之後,已經有越來越多的人開始使用 Kotlin。

結合著 Kotlin 的高級函數的特性可以讓代碼可讀性更強,更加簡潔,但是呢簡潔的背後是有代價的,使用不當對性能可能會有損耗,這塊往往很容易被我們忽略,這就需要我們去研究 kotlin 語法糖背後的魔法,當我們在開發的時候,選擇合適的語法糖,儘量避免這些錯誤,關於 Kotlin 性能損失那些事,可以看一下我另外兩篇文章。

[譯][2.4K Start] 放棄 Dagger 擁抱 Koin

https://juejin.im/post/5ebc1eb8e51d454dcf45744e

[譯][5k+] Kotlin 的性能優化那些事

https://juejin.im/post/5ec0f3afe51d454db11f8a94

這兩篇文章都分析了 Kotlin 使用不當對性能的影響,不僅如此 Kotlin 當中還有很多讓人傻傻分不清楚的語法糖例如 run, with, let, also, apply 等等,這篇文章將介紹一種簡單的方法來區分它們以及如何選擇使用。

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

如何使用 plus 操作符對集合進行操作?

當獲取 Map 值為空時,如何設置默認值?

require 或者 check 函數做什麼用的?

如何區分 run, with, let, also and apply 以及如何使用?

如何巧妙的使用 in 和 when 關鍵字?

Kotlin 的單例有幾種形式?

為什麼 by lazy 聲明的變量只能用 val?


在 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 集合中,可以使用 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 當中的用法。

https://github.com/hidhl/JDataBinding/blob/master/jdatabinding/src/main/java/com/hi/dhl/jdatabinding/DataBindingDialog.kt

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

感謝大神 Elye 的這篇文章提供的思路 Mastering Kotlin standard functions。

https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84

run, with, let, also, apply  都是作用域函數,這些作用域函數如何使用,以及如何區分呢,我們將從以下三個方面來區分它們。


是否是擴展函數


首先我們來看一下 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 關鍵字結合正則表達式,驗證用戶的輸入,這是一個很酷的技巧。


// 使用擴展函數重寫 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 單例總共有三種寫法:


使用 Object 實現單例

代碼:

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。

https://github.com/hi-dhl/JDataBinding
參考連結https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84https://proandroiddev.com/kotlin-fun-with-in-8a425704b635

推薦閱讀:

掃一掃 關注我的公眾號

如果你想要跟大家分享你的文章,歡迎投稿~

┏(^0^)┛明天見!

相關焦點

  • 為數不多的人知道的 Kotlin 技巧及解析(一)
    文章中沒有奇淫技巧,都是一些在實際開發中常用,但很容易被我們忽略的一些常見問題,源於平時的總結,這篇文章主要對這些常見問題進行分析。Kotlin 提供的高效的文件處理方法,以及原理解析?Sequence 和 Iterator 有那些不同之處?
  • Kotlin 怎麼學 ?遇到過哪些坑?
    然後就可以像 java 一樣使用 kotlin,當然這不是我們的最終目的,因為 kotlin 的語法糖才是我們最後的目標。google 發布了 kotlin 簡易教程,大概只需要幾個小時,就可以看完:在實際的開發中,大概第一個星期內,你寫代碼的速度會下降一些,但是一個星期之後,完全上手了,寫代碼的速度是有很大的提升的。
  • Kotlin入門教程,快使用Kotlin吧
    學習網站 Kotlin 從入門到放棄:https://www.jianshu.com/c/d3eac4c37b5fKotlin 菜鳥教程:http://www.runoob.com/kotlin/kotlin-tutorial.html基本語法 1、數據類型1.1、基本數據類型類型位寬度Double64Float32Long64Int32Short16Byte8每一個類型都有一個toXXX
  • 使用 Kotlin 進行 Android 測試
    這樣子我們就可以在任何環境下都不影響主應用的情況下放心地參試 Kotlin,體驗把這門現代接近成熟的語言帶來的激情,而且也可以為我們和我們的團隊做一些迎接大改變的準備。聽上去是不是很牛逼?那麼讓我們先寫些代碼...代碼敲起來大致上,我想展示我們怎麼使用 Kotlin 測試 Android 應用的。
  • Kotlin學習筆記——基礎篇
    1.基礎語法英文官網:https://kotlinlang.org/docs/reference/中文官網:https://www.kotlincn.net/docs/reference/協程學習資料:https://www.kotlincn.net/docs/reference/coroutines-overview.html
  • Kotlin 測試利器—MockK
    關鍵字  在 Kotlin 裡面 when是關鍵字,Mockito 的when ,必須加上反引號才能使用:`when`(xxxx).thenReturn(xxx)  如果看起來不舒服,也可以捨棄 Mockito 改用 mockito-kotlin
  • Kotlin項目實戰之手機影音---悅單條目實現及BaseListFragment抽取
    其中標紅的是一個平常寫布局的小小建議:為了能看到布局的效果不要直接將假的數據寫到View當中了【比如TextView中的android:text】,而要使用tools標籤來進行預覽數據的模擬【比如TextView中的tools:text】,這樣可以避免在線上可能會看到一些不想看到的假數據了
  • 問:談談 Kotlin 範型與逆變協變?
    kotlin 系列文章均以 Java 差異為核心進行提煉,與 Java 相同部分不再列出。隨著 kotlin 官方版本的迭代,文中有些語法可能會發生變化,請務必留意。kotlin 泛型(generics)與 java 一樣,kotlin 的泛型用來表示變量類型的參數化。
  • Kotlin 一統天下?Kotlin/Native 支持 iOS 和 Web 開發
    此外,還有一個名為 create-react-kotlin-app 的工具集 —— 通過 Kotlin 使用 React.js 創建現代 Web 應用。使用 create-react-kotlin-app,可讓開發者無需費心於項目設置和配置構建這些工作,能專注於快速開始客戶端應用開發,同時體會到靜態類型語言的優勢以及由 JavaScript 生態系統帶來的強大配套資源。詳情點此了解。
  • 學習 Kotlin 的 20 個實用資源
    (點擊上方公眾號,可快速關注)譯文:開源中國 - 達爾文原文:tutorialzine 如有好文章投稿,請點擊 → 這裡了解詳情Кotlin 是在 Java VM文章中簡單介紹了 Kotlin 帶來的一些優秀語法改進。
  • 最強總結 | 帶你快速搞定kotlin開發(上篇)
    fun sum(a: Int, b: Int): Int {     return a + b}  // 對於只有一行的函數,kotlin可以這麼簡寫,爽不?val可以理解為Java中的屬性聲明加上了final關鍵字(將kotlin的字節碼反編譯成Java一看就知道了),其實kotlin是更傾向於推薦使用val來聲明變量,這是一種防禦性的編碼思維模式,目的是減少程序出錯或者變的更加安全。
  • Kotlin函數式編程
    Lambda表達式在kotlin中,函數可以用lambda或者函數引用來表示。因此,任何以lambda或者函數引用作為參數或返回值的函數都是高階函數。3.1 lambda表達式的語法一個lambda把一小段行為進行編碼,可以將它當做一個值傳遞。可以被獨立的聲明並存儲到一個變量中。
  • 開源之Kotlin:如何通過AS用kotlin編程做簡單的安卓手機APP
    從今天起,我們開始以趣味小實例小應用的形式來接觸學習探討Android下的手機等移動端編程kotlin編程。需要說明的是,雖然這裡這裡說是Android,不過一人客的設想是,以kotlin編程和官方IDE(知名IDE)為工具,以手機或其他行動裝置為平臺,以應用為實現載體,大方向不變,但並不排除會根據現實情況或學習進度轉用其他OS比如國產OS鴻蒙等。
  • Kotlin使用指南之類篇特殊的類
    Kotlin中的一些特殊的類數據類```KOTLINdata class User(val name: String, val age: Int)```數據類就是在基本類的基礎上,自動從**主構造函數**中聲明的所有屬性導出1. equals()/hashCode() 對;2.
  • 新課程上線 | 學習使用 Kotlin 進行 Android 開發的最佳時機!
    Android Kotlin 基礎知識 (Android Basics in Kotlin)https://developer.android.google.cn/courses/android-basics-kotlin/coursehttps://www.hiringlab.org/2019/11/26/fastest-rising-tech-skills/自 Google 宣布將 Kotlin
  • Kotlin協程優雅的與Retrofit纏綿
    作者:limuyang2,連結:https://juejin.im/post/5cfb38f96fb9a07eeb139a00Kotlin已經成為Android開發的Google第一推薦語言,項目中也已經使用了很長時間的kotlin了,加上Kotlin1.3的發布,kotlin協程也已經穩定了
  • 程式語言 Kotlin 1.4 將推新的編譯器:今年春季發布
    為了提供一些背景信息,可以將編譯想成吸收源文件並將其逐步轉換為可執行代碼的管道。此管道的第一步俗稱為編譯器的前端。它解析代碼和命名、執行類型檢查等。此編譯器的這一部分也可以在 IDE 中使用,來高亮顯示語法錯誤、導航到定義並搜索項目中的符號用法。這是 kotlinc 如今花費最多時間的步驟,因此開發團隊希望使其更快。當前的實現尚未完成,並且不會在 1.4 中到來。
  • ...微信小程序將支持分享到朋友圈;Kotlin 1.4-M3 發布 | 極客頭條
    美團將成立優選事業部,入局社區團購 微信小程序將支持分享到朋友圈,適用於內容型頁面 阿里雲接到多家券商擴容需求,應對App卡頓等問題 滴滴計程車發布桔行系統:稱將圍繞該系統布局計程車信息化開放平臺 外媒
  • 使用Kotlin來開發Android,愛上它的優雅
    首先看看他的一些特性吧,之前看了一些Swift語言的特點,就先拿Kotlin和Swift和Java的對比來看看他的優勢吧。       Kotlin和Swift、Java              Kotlin 是一個基於 JVM 的新的程式語言,由 JetBrains開發。
  • 一款純Kotlin編寫的開源安卓應用 "Smile"
    首先,選擇一個完整的 kotlin 文檔,從第一章開始閱讀,尤其是最基礎的基本類型、控制流等等,千萬不能跳過,邊讀邊把知識點記錄下來,期間可以閱讀一些博客了解一下別人對這些知識點的理解,但是學習文檔是主線,一定不能偏離。這個過程我持續了三天,主要學習的文檔為: Kotlin 語言中文站 ,期間每學到一個知識點我就記錄下來,展示一部分: