[譯]Kotlin中內聯類(inline class)徹底解析(一)

翻譯說明:java

原標題: An Introduction to Inline Classes in Kotlin安全

原文地址: typealias.com/guides/intr…app

原文做者: Dave Leedside

不管你是編寫執行在雲端的大規模數據流程程序仍是低功耗手機運行的應用程序,大多數的開發者都但願他們的代碼可以快速運行。如今,Kotlin的最新實驗性的特性內聯類容許建立咱們想要的數據類型,而且還不會損失咱們須要的性能!函數

在這一系列新文章中,咱們將從上到下完全研究一番內聯類!post

在本篇文章中,咱們將會研究inline class是什麼, 它的工做原理是什麼以及在使用它的時候咱們如何去權衡選擇。而後,在接下來的文章中,咱們將深刻了解內聯類的內容,以確切瞭解它是如何實現的,並研究它如何與Java進行互操做。性能

請記住-這是一個實驗階段的語法特性,而且它正在被積極開發和完善。當前這篇文章是基於Kotlin-1.3M1版本的內聯類實現。測試

若是你想本身去嘗試使用它,我還寫了一篇配套文章how to enable them in your IDE,以便您能夠當即開始使用內聯類和其餘Kotlin 1.3功能!優化

強類型和普通值: 內聯類的案例

星期一早上8點,在給本身倒了一杯新鮮的熱氣騰騰的咖啡以後,而後在項目管理系統中領到一份任務。上面寫道:ui

向新用戶發送歡迎電子郵件 - 在註冊後四天

由於已經編寫好了郵件系統,您能夠啓動郵件調度程序的界面,正如你下面所看到的:

interface MailScheduler {
    fun sendEmail(email: Email, delay: Int)
}
複製代碼

看看這個函數,你知道你須要調用它...可是爲了將電子郵件延遲4天,你會傳遞什麼參數呢?

這個delay參數類型是Int. 因此咱們僅僅知道這是一個Integer,可是咱們並不知道它的單位是什麼-你是應該傳入4天呢? 或者它表明幾個小時,若是是這樣你傳入的應該是96(24 * 4)。又或者它的單位是分鐘、秒、毫秒...

咱們應該如何去優化這個代碼呢?

怎樣才能讓這個代碼變得更好呢?

若是編譯器可以強制指定正確的時間單位。例如,假設接收參數類型不是Int,讓咱們更新下interface中函數,讓它接收一個強類型Minutes

interface MailScheduler {
    fun sendEmail(email: Email, delay: Minutes)
}
複製代碼

如今咱們有了強類型系統爲咱們工做! 咱們不可能向這個函數發送一個Seconds類型參數,由於它只接受Minutes類型的參數!考慮如下代碼與先前版本相好比何可以在很大程度上減小錯誤:

val defaultDelay = Days(2)

fun send(email: Email) {
    mailScheduler.sendEmail(email, defaultDelay.toMinutes())
}
複製代碼

當咱們能夠充分利用類型系統時,咱們提升了代碼的健壯性。

可是開發者一般不會選擇去爲了單一普通值作個包裝器類,而更可能是經過傳遞Int、Float、Boolean這種基礎類型。

爲何會這樣呢?

一般,因爲性能緣由,咱們反對建立這樣的強類型。您可能還記得,JVM上的內存看起來像這樣:

當咱們建立一個基本類型的局部變量(即函數內定義的函數參數和變量)時 - 如Int、Float、Boolean - 這些值被存儲在部分JVM 內存堆棧中。將這些基礎類型的值存儲在堆棧上所涉及的性能開銷並不大。

在另外一方面,每當咱們實例化一個對象時,該對象實例就存儲在JVM堆上了。咱們在存儲和使用對象實例時會有性能損失 - 堆分配和內存提取的性能代價很高。雖然看起來每一個對象性能開銷微不足道,可是累積起來,它對代碼運行速度產生嚴重的影響。

若是咱們可以在不受性能影響的狀況下得到強類型系統的全部好處,那不是很好嗎?

實際上,Kotlin新特性inline class就是爲了解決這樣的問題而設計的。

讓咱們一塊兒來看看

內聯類的介紹

內聯類很容易去建立-僅僅須要在你定義的類前面加上inline關鍵字便可。

inline class Hours(val value: Int) {
    fun toMinutes() = Minutes(value * 60)
}
複製代碼

就是這樣!這個類如今將做爲您定義值的強類型,而且在許多狀況下,它和常規非內聯類相比性能成本幾乎相同。

您能夠像任何其餘類同樣實例化和使用內聯類。您最終可能須要在代碼中的某個位置引用裏面包裝的普通值 - 這個位置一般是在與另外一個庫或系統的邊界處。 固然,在這一點上,您能夠像一般使用任何其餘類同樣訪問這個值。

您應該知道的關鍵術語

內聯類包裝基礎類型的值。而且這個值也是有個類型的,咱們把它稱之爲基礎類型

爲何內聯類能夠高性能執行

那麼,內聯類爲何能夠和普通類更好地執行呢?

你能夠像這樣去實例化一個內聯類

val period = Hours(24)
複製代碼

...實際上該類並未在編譯代碼中實例化!事實上,就JVM而言,實際上至關於下面這樣的代碼......

int period = 24;
複製代碼

正如您所看到的,在此編譯版本的代碼中沒有Hours概念 - 它只是將基礎值分配給int類型的變量! 一樣,當您使用內聯類做爲函數參數的類型時也是這樣的:

fun wait(period: Hours) { /* ... */ }
複製代碼

...它能夠有效地編譯成以下這樣......

void wait(int period) { /* ... */ }
複製代碼

所以,咱們的代碼中內聯了基礎類型和基礎值。換句話說,編譯後的代碼只使用了int整數類型,所以咱們避免了在堆內存上建立和訪問對象的開銷成本。

可是請等一下!

還記得Hours類有一個名爲toMinutes()的函數嗎?由於編譯後的代碼使用的是int而不是Hours對象實例,所以想像一下調用toMinutes()函數時會發生什麼呢?

inline class Hours(val value: Int) {
    fun toMinutes() = Minutes(value * 60)
}
複製代碼

Hours.toMinutes()的編譯代碼以下所示:

public static final int toMinutes(int $this) {
	return $this * 60;
}
複製代碼

若是咱們在Kotlin中調用Hours(24).toMinutes(),它能夠有效地編譯爲toMinutes(24).

沒問題,確實能夠像這樣處理函數,可是類成員屬性呢?若是咱們但願Hours除了主要的基礎值以外還包括其餘一些數據,該怎麼辦?

一切事情都是有它的權衡的,那麼這是其中之一 - 內聯類除了基礎值以外不能有任何其餘成員屬性。讓咱們探討其餘的。

權衡和使用限制

如今咱們知道內聯類能夠經過編譯代碼中的基礎值來表示,咱們已經準備好了解使用它們時應注意哪些使用限制。

首先,內聯類必須包含一個基礎值,這就意味它須要一個主構造器來接收 這個基礎值,此外它必須是隻讀的(val)。你能夠定義你想要的基礎值變量名。

inline class Seconds()              // nope - needs to accept a value!
inline class Minutes(value: Int)    // nope - value needs to be a property
inline class Hours(var value: Int)  // nope - property needs to be read-only
inline class Days(val value: Int)   // yes!
inline class Months(val count: Int) // yes! - name it what you want
複製代碼

若是有須要,能夠將該屬性設爲私有的,但構造函數必須是公有的。

inline class Years private constructor(val value: Int) // nope - constructor must be public
inline class Decades(private val value: Int)           // yes!
複製代碼

內聯類中不能包含init block初始化塊。我會在下一篇發表的文章中探討內聯類如何與Java進行互操做,這點將會完全說明白。

inline class Centuries(val value: Int) {
	// nope - "Inline class cannot have an initializer block"
    init { 
        require(value >= 0)
    }
}
複製代碼

正如咱們在上面發現的那樣,除了一個基礎值以外,咱們的內聯類主構造器不能包含其餘任何成員屬性。

// nope - "Inline class must have exactly one primary constructor parameter"
inline class Years(val count: Int, val startYear: Int)
複製代碼

可是呢,它的內部是能夠擁有成員屬性的,只要它們僅基於構造器中那個基礎值計算,或者從能夠靜態解析的某個值或對象計算 - 來自單例,頂級對象,常量等。

object Conversions {
    const val MINUTES_PER_HOUR = 60    
}

inline class Hours(val value: Int) {
    val valueAsMinutes get() = value * Conversions.MINUTES_PER_HOUR
}
複製代碼

不容許類繼承 - 內聯類不能繼承另外一個類,而且它們不能被另外一個類繼承。 (Kotlin 1.3-M1在技術上確實容許內聯類繼承另外一個類,但在即將發佈的版本中會對此進行更正)

open class TimeUnit
inline class Seconds(val value: Int) : TimeUnit() // nope - cannot extend classes

open inline class Minutes(val value: Int) // nope - "Inline classes can only be final"
複製代碼

若是您須要將內聯類做爲子類型,那很好 - 您能夠實現接口而不是繼承基類。

interface TimeUnit {
	val value: Int
}

inline class Hours(override val value: Int) : TimeUnit  // yes!
複製代碼

內聯類必須在頂級聲明。嵌套/內部類不能內聯的。

class Outer {
	 // nope - "Inline classes are only allowed on top level"
    inline class Inner(val value: Int)
}

inline class TopLevelInline(val value: Int) // yes!
複製代碼

目前,也不支持內聯枚舉類。

// nope - "Modifier 'inline' is not applicable to 'enum class'"
inline enum class TimeUnits(val value: Int) {
    SECONDS_PER_MINUTE(60),
    MINUTES_PER_HOUR(60),
    HOURS_PER_DAY(24)
}
複製代碼

Type Aliases(類型別名) 與 Inline Classes(內聯類)對比

由於它們都包含基礎類型,因此內聯類很容易與類型別名混淆。可是有一些關鍵的差別使它們在不一樣的場景下得以應用。

類型別名爲基礎類型提供備用名稱。例如,您能夠爲String這樣的常見類型添加別名,併爲其指定在特定上下文中有意義的描述性名稱,好比UsernameUsername類型的變量其實是源代碼和編譯代碼中String類型的變量同一個東西,只是不一樣名稱而已。例如,您能夠這樣作:

typealias Username = String

fun validate(name: Username) {
    if(name.length < 5) {
        println("Username $name is too short.")
    }
}
複製代碼

注意到咱們是能夠在name上直接調用.length的,這是由於name實際上就是個String,儘管咱們在聲明參數類型的時候使用的是別名Username.

在另外一面,內聯類其實是基礎類型的包裝器,所以當你須要使用基礎值的時候,須要作拆箱操做。例如咱們使用內聯類來重寫上面別名的例子:

inline class Username(val value: String)

fun validate(name: Username) {
    if (name.value.length < 5) {
        println("Username ${name.value} is too short.")
    }
}
複製代碼

注意到咱們是必須這樣name.value.length而不是name.length,咱們必須解開這個包裝器取出裏面的值。

可是最大的區別在於與分配兼容性有關。內聯類爲你提供類型的安全性,類型別名則沒有。 類型別名與其基礎類型相同。例如,看以下代碼:

typealias Username = String
typealias Password = String

fun authenticate(user: Username, pass: Password) { /* ... */ }

fun main(args: Array<String>) {
    val username: Username = "joe.user"
    val password: Password = "super-secret"
    authenticate(password, username)
}
複製代碼

在這種狀況下,UsernamePassword僅僅是String另外一個不一樣名稱而已,甚至你能夠將UsernamePassword任意調換位置。實際上,這正是咱們在上面的代碼中所作的 - 當咱們調用authenticate()函數時,即便咱們將UsernamePassword位置弄反了,但編譯器依然認爲是合法的。

另外一方面,若是你對上面同一個案例使用內聯類,那麼編譯器將會很幸運告訴你這是不合法的:

inline class Username(val value: String)
inline class Password(val value: String)

fun authenticate(user: Username, pass: Password) { /* ... */ }

fun main(args: Array<String>) {
    val username: Username = Username("joe.user")
    val password: Password = Password("super-secret")
    authenticate(password, username) // <--- Compiler error here! =)
}
複製代碼

這點很是強大!這強大到寫代碼時候就告訴咱們,咱們寫了一個bug。咱們無需等待自動測試、QA工程師或用戶告訴咱們。很是棒!

包裝

你本身準備開始嘗試內聯類了嗎? 若是是,請當即閱讀 how to enable inline classes

雖然咱們已經介紹了相關的基礎知識,但在使用它們時要記住一些使人困惑的注意和限制。實際上,若是你不瞭解內部的原理,使用內聯類可能會寫出比正常普通類運行速度更慢的代碼。

在下一篇文章中,咱們將深刻研究內聯類底層工做原理,以便於你在運用的時候更加高效。

譯者有話說

還記得上篇Kotlin新特性的文章嗎,實際上關於inline class內容在上篇基本講的很清楚了。可是上篇文章篇幅有限,特定找了一篇比較全面的inline class相關英文文章再次梳理和鞏固內聯類的知識。而且原文做者更是寫了一系列有關inline class的文章,從它的使用到基本介紹最後到剖析內部原理,講得很是清楚。固然我還會繼續翻譯他的最後一篇深刻inline class內部原理文章。歡迎你們持續關注~~~

Kotlin系列文章,歡迎查看:

原創系列:

翻譯系列:

實戰系列:

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

相關文章
相關標籤/搜索