連結:
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 函數,實現一行代碼交換兩個變量?我們先來回顧一下 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 單例總共有三種寫法:
代碼:
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推薦閱讀:
掃一掃 關注我的公眾號
如果你想要跟大家分享你的文章,歡迎投稿~
┏(^0^)┛明天見!