Kotlin 1.4.30-M1 加強的內聯類是個什麼東西?

關鍵詞:Kotlin Newsjava

內聯類從 1.3 推出,一直處於實驗狀態。程序員

內聯類 inline class,是從 Kotlin 1.3 開始加入的實驗特性,計劃 1.4.30 進入 Beta 狀態(看來 1.5.0 要轉正了?)。web

內聯類要解決的問題呢,其實也與以往咱們接觸到的內聯函數相似,大致思路就是提供某種語法,提高代碼編寫體驗和效率,同時又藉助編譯器的優化手段來減小這樣作的成本。面試

1. 從內聯函數提及

咱們先以各種編程語言當中普遍存在的內聯函數爲例來講明內聯的做用。編程

函數調用時有成本的,這涉及到參數的傳遞,結果的返回,調用棧的維護等一系列工做。所以,對於一些比較小的函數,能夠在編譯時使用函數的內容替換函數的調用,以減小函數的調用層次,例如:安全

fun max(a: Int, b: Int)Int = if(a > b) a else b

fun main() {
    println(max(12))
}

在 main 函數當中調用 max 函數,從代碼編寫的角度來看,使用函數 max 讓咱們的代碼意圖更加明顯,也使得求最大值的邏輯更容易複用,所以在平常的開發當中咱們也一直鼓勵你們這樣作。微信

不過,這樣的結果就是一個簡單的比較大小的事兒變成了一次函數的調用:編程語言

  public final static main()V
   L0
    LINENUMBER 6 L0
    ICONST_1
    ICONST_2
    INVOKESTATIC com/bennyhuo/kotlin/InlineFunctionKt.max (II)I
    INVOKESTATIC kotlin/io/ConsoleKt.println (I)V

若是咱們把 max 聲明成內聯函數:編輯器

inline fun max(a: Int, b: Int)Int = if(a > b) a else b

結果就不同了:ide

  public final static main()V
   L0
    LINENUMBER 6 L0
    ICONST_1
    ISTORE 0
    ICONST_2
    ISTORE 1
   L1
    ICONST_0
    ISTORE 2
   L2
    LINENUMBER 8 L2
   L3
    ILOAD 1
   L4
   L5
    ISTORE 0
   L6
    LINENUMBER 6 L6
   L7
    ICONST_0
    ISTORE 1
   L8
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream
;
    ILOAD 0
    INVOKEVIRTUAL java/io/PrintStream.println (I)V

這樣咱們就已經看不到 max 函數的調用了。

固然,對於這樣的小函數,編譯器和運行時已經足夠聰明到能夠本身自動作優化了,內聯函數在 Kotlin 當中最大的做用實際上是高階函數的內聯,咱們就以最爲常見的 forEach 爲例:

inline fun <T> Array<out T>.forEach(action: (T) -> Unit)Unit {
    for (element in this) action(element)
}

forEach 函數被聲明爲 inline,這說明它是一個內聯函數。按照咱們的前面對內聯函數的理解,下面的代碼:

arrayOf(1,2,3,4).forEach {
    println(it)
}

編譯以後大體至關於:

for (element in arrayOf(1,2,3,4)) {
    { it: Int -> println(it) }(element)
}

這樣 forEach 自身的調用就被消除掉了。不過,這還不夠,由於咱們看到 { it: Int -> println(it) }(element) 其實就是前面 forEach 定義當中的 action(element),這也是一個函數調用,也是有成本的。更爲甚者,每一次循環當中都會建立一個函數對象(Lambda)而且調用它,這樣一來,還會有頻繁建立對象的開銷。

因此,Kotlin 當中的內聯函數也會同時對函數類型的參數進行內聯,所以前面的調用編譯以後實際上至關於:

for (element in arrayOf(1,2,3,4)) {
    println(element)
}

並且這樣也更符合咱們的直覺。

總結一下,內聯函數能夠減小函數對象的建立和函數調用的次數。

提問:因此你知道爲何 IDE 會對 max 這樣的非高階函數的內聯發出警告了嗎?

2. 什麼是內聯類

內聯函數能夠減小對象的建立,內聯類實際上也是如此。

內聯類實際上就是對其餘類型的一個包裝,就像內聯函數實際上是對一段代碼的包裝同樣,在編譯的時候對於內聯類對象的訪問都會被編譯器拆掉包裝而獲得內部真實的類型。所以,內聯類必定有且只有一個屬性,並且這個屬性還不能被修改。

內聯類的語法其實也簡單,與 Kotlin 當中其餘的枚舉類、密封類、數據類的定義方式相似,在 class 前面加一個 inline 便可:

inline class PlayerState(val value: Int)

使用時大多數狀況下就像普通類型那樣:

val idleState = PlayerState(0)
println(idleState.value)

雖然這裏建立了一個 PlayerState 的實例 idleState,咱們也對這個實例的成員 value 進行了訪問,但編譯完以後這段代碼大體至關於:

val value = 0
println(value)

由於 PlayerState 這個類型的實例被內聯,結果就剩下 value 自己了。

咱們固然也能夠給內聯類定義其餘成員,這其中包括無狀態的屬性(沒有 backing field)和函數:

inline class PlayerState(val value: Int) {
    val isIdle
        get() = value == 0
    
    fun isPlaying() = value == 1
}

訪問這些成員的時候,編譯器也並不會將內聯類的實例建立出來,而是轉換成靜態方法調用:

val idleState = PlayerState(0)
println(idleState.isIdle)
println(idleState.isPlaying())

於是就至關於:

val value = 0
println(PlayerState.isIdle-impl(value))
println(PlayerState.isPlaying-impl(value))

isIdle-implisPlaying-impl 這兩個函數是編譯器自動爲 PlayerState 生成的靜態方法,它們的方法名中加了 - 這樣的非法字符,這意味着這些方法對於 Java 來說是不友好的,換句話講,內聯類不能與 Java 的語法兼容。

咱們再看一個稍微複雜的情形:

val idleState = PlayerState(0)
println(idleState)

咱們直接將這個內聯類的實例傳給 println,這下編譯器會怎麼辦呢?編譯器只會在儘量須要的狀況下完成內聯,但對於這種強制須要內聯類實例的狀況,也是沒法繞過的,所以在這裏會發生一次「裝箱」操做,把內聯類實例真正建立出來,大體至關於:

val value = 0
println(PlayerState(value))

簡單總結一下就是:

  1. 在必定範圍內,內聯類能夠像普通類那樣使用。言外之意,其實內聯類也有挺多限制的,這個咱們待會兒再聊。
  2. 編譯以後,編譯器會盡量地將內聯類的實例替換成其成員,減小對象的建立。

3. 內聯類有什麼限制?

經過前面對於內聯類概念的討論,咱們已經知道內聯類

  1. 有且僅有一個不可變的屬性
  2. 能夠定義其餘屬性,但不能有狀態

實際上,因爲內聯類存在狀態限制,所以內聯類也不能繼承其餘類型,但這不會影響它實現接口,例如標準庫當中的無符號整型 UInt 定義以下:

inline class UInt internal constructor(internal val dataInt) : Comparable<UInt> {
  ...

  override inline operator fun compareTo(other: UInt)Int = uintCompare(this.data, other.data)

  ...
}

這個例子裏面其實還有驚喜,那就是 UInt 的構造器是 internal 的,若是你想要同樣畫葫蘆在本身的代碼當中這樣寫,怕是要看一下編譯器的臉色了:

如下爲 Kotlin 1.4.20 當中的效果

在 Kotlin 1.4.30 之前,內聯類的構造器必須是 public 的,這意味着在過去咱們不能經過內聯類來完成對某一種特定類型的部分值的包裝:由於外部同樣能夠創造出來新的內聯類實例。

不過,1.4.30-M1 當中已經解除了這一限制,詳情參見:KT-28056 Consider supporting non-public primary constructors for inline classes(https://youtrack.jetbrains.com/issue/KT-28056),於是咱們如今能夠將內聯類的構造器聲明爲 internal 或者 private,以防止外部隨意建立新實例:

inline class PlayerState
private constructor(val value: Int) {
    companion object {
        val error = PlayerState(-1)
        val idle = PlayerState(0)
        val playing = PlayerState(1)
    }
}

這樣,PlayerState 的實例就僅限於 error、idle、playing 這幾個了。

除了前面限制實例的場景,有些狀況下咱們其實只是但願經過內聯類提供一些運行時的校驗,這就須要咱們在 init 塊當中來完成這樣的工做了,但內聯類的 init 塊在 1.4.30 之前也是禁止的:

1.4.30-M1 開始解除了這一限制,詳情參見:KT-28055 Consider supporting init blocks inside inline classes(https://youtrack.jetbrains.com/issue/KT-28055)。不過須要注意的是,雖然 init 塊當中的邏輯只在運行時有效,但這樣的特性可讓被包裝類型的值與它的條件在代碼當中緊密結合起來,提供更良好的一致性。

4. 內聯類有什麼應用場景?

前面在討論內聯類的概念和限制時,咱們已經給出了一些示例,你們也大概可以想到內聯類的具體做用。接下來咱們再總體梳理一下內聯類的應用場景。

4.1 增強版的類型別名

內聯類最一開始給人的感受就是「類型別名 Plus」,由於內聯類在運行時會被儘量替換成被包裝的類型,這與類型別名看上去很接近。不過,類型別名本質上就是一個別名,它不會致使新類型的產生,而內聯類是確實會產生新類型的:

inline class Flag0(val value: Int)
typealias Flag1 = Int

fun main() {
    println(Flag0::class == Int::class) // false
    println(Flag1::class == Int::class) // true
    
    val flag0 = Flag0(0)
    val flag1 = 0
}

4.2 替代枚舉類

內聯類在 1.4.30 以後能夠經過私有化構造函數來限制實例個數,這樣也能夠達到枚舉的目的,咱們前面已經給出過例子:

內聯類的寫法

inline class PlayerState
private constructor(val value: Int) {
    companion object {
        val error = PlayerState(-1)
        val idle = PlayerState(0)
        val playing = PlayerState(1)
    }
}

枚舉類的寫法

enum class PlayerState {
    error, idle, playing
}

咱們還能夠爲內聯類添加各類函數來加強它的功能,這些函數最終都會被編譯成靜態方法:

inline class PlayerState
private constructor(val value: Int) {
    companion object {
        val error = PlayerState(-1)
        val idle = PlayerState(0)
        val playing = PlayerState(1)
        
        fun values() = arrayOf(error, idle, playing)
    }
    
    fun isIdle() = this == idle
}

雖然內聯相似乎寫起來稍微囉嗦了一些,但在內存上卻跟直接使用整型幾乎是同樣的效果。

話說到這兒,不知道你們是否是能想起 Android 當中的註解 IntDef,結果上都是使用整型來替代枚舉,但內聯類顯然更安全,IntDef 只是一種提示而已。不只如此,內聯類也能夠用來包裝字符串等其餘類型,無疑將是一種更加靈活的手段。

固然,使用的內聯類相較於枚舉類有一點點小缺點,那就是使用 when 表達式時必須添加 else 分支:

使用內聯類

val result = when(state) {
  PlayerState.error -> { ... }
  PlayerState.idle -> { ... }
  PlayerState.playing -> { ... }
  else -> { ... } // 必須,由於編譯器沒法推斷出前面的條件是完備的
}

而因爲編譯器可以肯定枚舉類的實例可數的,所以 else 再也不須要了:

使用枚舉類

val result = when(state) {
  PlayerState.error -> { ... }
  PlayerState.idle -> { ... }
  PlayerState.playing -> { ... }
}

4.3 替代密封類

密封類用於子類可數的場景,枚舉類則用於實例可數的場景。

咱們前面給出的 PlayerState 其實不夠完善,例如狀態爲 error 時,也應該同時附帶錯誤信息;狀態爲 playing 時也應該同時有歌曲信息。顯然當前一個簡單的整型是作不到這一點的,所以咱們很容易能想到用密封類替代枚舉:

class Song {
  ...
}

sealed class PlayerState

class Error(val t: Throwable): PlayerState()
object Idle: PlayerState()
class Playing(val song: Song): PlayerState()

若是應用場景對於內存不敏感,這樣寫實際上一點兒問題都沒有,並且代碼的可讀性和可維護性都會比狀態值與其相對應的異常和播放信息獨立存儲要強得多。

這裏的 Error、Playing 這兩個類型其實就是包裝了另外的兩個類型 Throwable 和 Song 而已,是否是咱們能夠把它們定義爲內聯類呢?直接定義確定是不行的,由於 PlayerState 是個密封類,密封類本質上也是一個類,咱們前面提到過內聯類有不能繼承類型的限制,當時給出的理由是內聯類不能包含其餘狀態。這樣看來,若是父類當中足夠簡單,不包含狀態,是否是未來有但願支持繼承呢?

其實問題不僅是狀態那麼簡單,還有多態引起的裝箱和拆箱的問題。由於一旦涉及到父類,內聯類不少時候都沒法實現內聯,咱們假定下面的寫法是合法的:

sealed class PlayerState

inline class Error(val t: Throwable): PlayerState()
object Idle: PlayerState()
inline class Playing(val song: Song): PlayerState()

那麼:

var state: PlayerState = Idle
...
state = Error(IOExeption("...")) // 必須裝箱,沒法內聯
...
state = Playing(Song(...)) // 必須裝箱,沒法內聯

這裏內聯機制就失效了,由於咱們沒法將 Song 的實例直接賦值給 state,IOException 的實例也是如此。

不過,做爲變通,其實咱們也能夠這樣改寫上面的例子:

inline class PlayerState(val state: Any?) {
    init {
        require(state == null || state is Throwable || state is Song)
    }
    
    fun isIdle() = state == null
    fun isError() = state is Throwable
    fun isPlaying() = state is Song
}

這樣寫就與標準庫當中大名鼎鼎的 Result 類有殊途同歸之妙了:

inline class Result<out Tinternal constructor(
    internal val value: Any?
) : Serializable {

  val isSuccess: Boolean get() = value !is Failure

  val isFailure: Boolean get() = value is Failure
  
  ...
}

5. 小結

本文咱們簡單介紹了一下內聯類的做用,實現細節,以及使用場景。簡單總結以下:

  1. 內聯類是對其餘類實例的包裝
  2. 內聯類在編譯時會盡量地將實例替換成被包裝的對象
  3. 內聯類的函數(包括無狀態屬性)都將被編譯成靜態函數
  4. 內聯類在內存敏感的場景下能夠必定程度上替代枚舉類、密封類的使用
  5. 內聯類不能與 Java 兼容

C 語言是全部程序員應當認真掌握的基礎語言,無論你是 Java 仍是 Python 開發者,歡迎你們關注個人新課 《C 語言系統精講》,上線一個月已經有 400 位同窗在一塊兒學習了:

掃描二維碼便可進入課程啦!


Kotlin 協程對大多數初學者來說都是一個噩夢,即使是有經驗的開發者,對於協程的理解也仍然是懵懵懂懂。若是你們有一樣的問題,不妨閱讀一下個人新書《深刻理解 Kotlin 協程》,完全搞懂 Kotlin 協程最難的知識點:


若是你們想要快速上手 Kotlin 或者想要全面深刻地學習 Kotlin 的相關知識,能夠關注基於 Kotlin 1.3.50 的 《Kotlin 入門到精通》

掃描二維碼便可進入課程啦!


Android 工程師也能夠關注下《破解Android高級面試》,這門課涉及內容均非淺嘗輒止,除知識點講解外更注重培養高級工程師意識,目前已經有 1100 多位同窗在學習:

掃描二維碼便可進入課程啦!


本文分享自微信公衆號 - Kotlin(KotlinX)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索