分享一篇很有用的乾貨,希望對大家的學習有所啓發和幫助~
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 中已收錄,裏面包含不同方向的自學編程路線、面試題集合/面經、及系列技術文章等,資源持續更新中...