1. 程式人生 > 程式設計 >關於Kotlin中SAM轉換的那些事

關於Kotlin中SAM轉換的那些事

前言

隨著 Kotlin 1.4 正式釋出,關於 SAM 轉換的一些問題就可以蓋棺定論了。因為這裡要講的都是些舊的東西,所以這是一篇灌水文。

Kotlin對SAM轉換的支援情況

在 1.4 釋出之前,經常有新人在群裡提出關於 SAM 轉換的問題。

為了說明這個問題,要分成幾個情況來討論。

我們需要區分這個介面是Java介面還是Kotlin介面:

// 這是Java
interface JavaSome {
 void some(); 
}
// 這是Kotlin
interface KotlinSome {
 fun some()
}

以及區分在Java還是Kotlin裡使用該介面:

// 這是Java,ISome是一個介面
void useSome(ISome some) {}
// 這是Kotlin,ISome是一個介面
fun useSome(some: ISome) {}

兩兩相乘,我們就需要對4種情況進行討論。當然,useSome 函式都是在 Kotlin 裡呼叫。

1、Java介面,Java使用

// Java
void useSome(JavaSome some) {}
// Kotlin
useSome {} // OK

這種情況下的 SAM 轉換,是自古以來 Kotlin 就支援的。

2、Java介面,Kotlin使用

// Kotlin
fun useSome(some: JavaSome) {}

useSome {} // 能否編譯成功跟Kotlin版本和編譯器引數有關

Kotlin 1.2 以及更舊版本不支援這種情況下的SAM轉換。

Kotlin 1.3 版本,Kotlin 官方團隊發現他們寫的那堆型別推斷演算法是一座“屎山”,於是重新寫了套新的型別推斷演算法,作為預設關閉的實驗性特性加入了 1.3 版本。新的型別推斷演算法支援這種情況下的SAM轉換,不過需要手動傳入編譯器引數來開啟這個功能。

Kotlin 1.4 版本,由於新的型別推斷演算法已經預設開啟,所以這種情況下可以進行SAM轉換。

3、Kotlin介面,Kotlin使用

// Kotlin
fun useSome(some: KotlinSome) {}

useSome {} // 編譯錯誤!

這就是廣為人知、為人詬病的垃圾 Kotlin 不支援 SAM 轉換的情況。

在 Kotlin 1.4 版本,你需要在介面前加上關鍵字 fun,讓它成為一個 fun interface 才能享受到 SAM 轉換。

// Kotlin
fun interface KotlinSome {
 fun some()
}

fun useSome(some: KotlinSome) {}

useSome {} // OK

當然 1.3 版本就別想了,老老實實升級吧。

4、Kotlin介面,Java使用

// Java
void useSome(KotlinSome some) {}
// Kotlin
useSome {} // 需要是 fun interface

非常少見。

和上面的第三種情況一樣,這需要 Kotlin 1.4 版本的 fun interface 才能進行 SAM 轉換。

5、帶有suspend函式的Kotlin介面

四天王有五個人不是常識麼

fun interface Some {
 suspend fun some()
}

fun useSome(some: KotlinSome) {}

useSome {} // 嘻嘻

在 Kotlin 1.4 的測試版(里程碑版、RC版),可以編譯成功,但是執行起來會炸。原因在於 Kotlin 官方團隊並沒有寫好針對這種情況的程式碼生成(codegen)。於是在 Kotlin 1.4 正式版,他們就 ban 掉了這樣的程式碼,不允許 fun interface 擁有抽象 suspend 函式。

6、一些舊版本的bug

最經典的是那個安卓的LiveData的某個函式:

val liveData = MutableLiveData<Int>()
liveData.observe({ lifecycleOwner.lifecycle },Observer { invokeMyMethod(it) })
// 第二個引數無法進行SAM轉換

詳見KT-14984。

新的型別推斷演算法修正了這個bug。

SAM Constructor

在 1.3 以及更早的版本,針對上面所說的第二種情況,可以這樣使用:

// Kotlin
fun useSome(some: JavaSome) {}

useSome(JavaSome {})

想必各位過來人都知道這樣的寫法。

這裡 JavaSome {},lambda 表示式前面的那個 JavaSome 就是所謂的 SAM 構造器(SAM constructor),或者說是 SAM 介面卡(SAM adapter)。

在現在 1.4 版本里,SAM constructor 已經沒什麼用了,主要用途是“憑空捏出”一個 SAM 介面的例項:

val ktSome = KotlinSome {} // 需要是 fun interface
val javaSome = JavaSome {}

// 錯誤用法
// val ktSome: KotlinSome = {}
// val javaSome: JavaSome = {}

SAM constructor 可以理解為編譯器為 SAM 介面生成了一個如下所示的輔助函式,但是實際上這個函式並不存在。

// 這是Java
interface JavaSome {
 void some(); 
}
// 實際上並不存在的輔助函式
inline fun JavaSome(block: () -> Unit): JavaSome {
 return 編譯器的魔法
}

然後就有一些鮮為人知的用法,比如說這樣:

// Kotlin
val lambda: () -> Unit = { println("test") }
val kepa: JavaSome = JavaSome(lambda) // 嘻嘻
kepa.some() // 輸出 test

上面這段程式碼確實是可以跑的。

甚至是這樣:

val lambda: () -> Unit = { println("test") }
val some: KFunction1<() -> Unit,JavaSome> = ::JavaSome // 嘻嘻
val kepa: JavaSome = some.invoke(lambda)
kepa.some()

這段程式碼 IDEA 不會提示錯誤,但是會編譯失敗。

表面上看確實有這個輔助函式,所以這樣的程式碼可以通過 Kotlin 編譯器前端的檢查。但是實際上編譯器的後端並沒有辦法針對這樣的情況進行程式碼生成,徹底懵逼了,boom!

你學到了什麼

  • 一些無用的歷史知識
  • 關於 SAM constructor 的冷知識

本文完。

總結

到此這篇關於關於Kotlin中SAM轉換的文章就介紹到這了,更多相關Kotlin中SAM轉換內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!