絕對乾貨 | Kotlin內聯類工做原理及使用案例,看完你會回來謝個人

初看內聯類

內聯類很是的簡單,您只須要在類的前面加上inline關鍵字就能夠:android

inline class WrappedInt(val value: Int)

內聯類有一些或多或少明顯的限制:須要在主構造函數中精確指定一個屬性,如value所示。您不能在一個內聯類中包裝多個值。內聯類中也禁止包含init塊,而且不能具備帶有幕後字段的屬性。內聯類能夠具備簡單的可計算屬性,可是咱們將在本文後面看到。web

在運行時,將盡量使用內聯類的包裝類型而不使用其包裝。這相似於Java的框式類型,例如Integer或Boolean,只要編譯器能夠這樣作,它們就會被表示爲它們對應的原始類型。這正是Kotlin中內聯類的一大賣點:內聯類時,除非絕對必要,不然類自己不會在字節碼中使用。內聯類大大減小了運行時的空間開銷。json

運行時

在運行時,能夠將內聯類表示爲包裝類型和基礎類型。如前一段所述,編譯器更喜歡使用內聯類的基礎(包裝)類型來儘量地優化代碼。這相似於int和Integer之間的裝箱。可是,在某些狀況下,編譯器須要使用包裝器自己,所以它將在編譯期間生成:安全

public final class WrappedInt {
   private final int value;

   public final int getValue() return this.value; }

   // $FF: synthetic method
   private WrappedInt(int value) this.value = value; }

   public static int constructor_impl(int value) return value; }

   // $FF: synthetic method
   @NotNull
   public static final WrappedInt box_impl(int v) return new WrappedInt(v); }

   // $FF: synthetic method
   public final int unbox_impl() return this.value; }

   //more Object related implementations
}

此代碼段顯示了內聯類簡化的Java字節碼。除了一些顯而易見的東西,例如value字段及其getter以外,構造函數是私有的,而新對象將經過Constructor_impl建立,該對象實際上並不使用包裝器類型,而僅返回傳入的基礎類型。最後,您能夠看到box_impl和unbox_impl函數,可能如您所指望的,它們的目的在於拆裝箱的操做。如今,讓咱們看看在代碼中如何使用內聯類。微信

使用內聯類

fun take(w: WrappedInt) {
    println(w.value)
}

fun main() {
    val inlined = WrappedInt(5)
    take(inlined)
}

在此代碼段中,正在建立WrappedInt並將其傳遞給打印其包裝值的函數。相應的Java字節碼,以下所示:app

public static final void take_hqTGqkw(int w) {
    System.out.println(w);
}

public static final void main() {
    int inlined = WrappedInt.constructor_impl(5);
    take_hqTGqkw(inlined);
}

在已編譯的代碼中,沒有建立WrappedInt實例。儘管使用了靜態的builder_impl函數,它只是返回一個int值,而後將其傳遞給take函數,該函數也對咱們最初在源代碼中擁有的內聯類的類型一無所知。請注意,接受內聯類參數的函數名稱會用字節碼中生成的哈希碼擴展。這樣,它們能夠與接受基礎類型做爲參數的重載函數區分開:編輯器

fun take(w: WrappedInt) = println(w.value)
fun take(v: Int) = println(v.value)

爲了使這兩種take方法在JVM字節碼中可用並避免簽名衝突,編譯器將第一個方法重命名爲take-hqTGqkw之類的東西。注意,上面的示例確實顯示了「 _」而不是「-」,由於Java不容許方法名稱包含破折號,這也是爲何不能從Java調用接受內聯類的方法的緣由。函數

內聯類的裝箱

前面咱們看到過,box_impl和unbox_impl函數是爲內聯類建立的,那麼何時須要它們?Kotlin的文檔引用了一條經驗法則:工具

內聯類在用做其餘類型時會被裝箱flex

例如,當您將內聯類用做通用類型或可爲空的類型時,就會發生裝箱:

inline class WrappedInt(val value: Int)

fun take(w: WrappedInt?) 
{
    if (w != null) println(w.value)
}

fun main() {
    take(WrappedInt(5))
}

在此代碼中,咱們修改了take函數以採用可爲空的WrappedInt,並在參數不爲null時顯示基礎類型。

public static final void take_G1XIRLQ(@Nullable WrappedInt w) {
    if (Intrinsics.areEqual(w, (Object)null) ^ true) {
        int var1 = w.unbox_impl();
        System.out.println(var1);
    }
}

public static final void main() {
    take_G1XIRLQ(WrappedInt.box_impl(WrappedInt.constructor_impl(5)));
}

在字節碼中,take函數如今再也不直接接受基礎類型。它必須改成使用裝箱類型。打印其內容時,將調用unbox_impl。在調用的地方,咱們能夠看到box_impl用於建立WrappedInt的裝箱實例。

顯然,咱們但願儘量避免裝箱。請記住,內聯類以及原始類型的特定用法一般都依賴於此技術,所以可能必須從新考慮是否該這麼作。

使用案例

咱們看到內聯類具備巨大的優點:在最佳狀況下,因爲避免了額外的堆分配,它們能夠大大減小運行時的開銷。可是咱們何時適合使用這種包裝類型呢?

更好的區分類型

假若有一個身份驗證方法API,以下所示:

fun auth(userName: String, password: String) { println("authenticating $userName.") }

在一個美好的世界中,每一個人都會用用戶名和密碼來稱呼它。可是,某些用戶將以不一樣的方式調用此方法並不困難:

auth("12345""user1")

因爲這兩個參數均爲String類型,所以您可能會弄亂它們的順序,固然,隨着參數數量的增長,這種順序的可能性更大。這些類型的包裝類型能夠幫助您減輕這種風險,所以內聯類是一個很棒的工具:

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

fun auth(userName: UserName, password: Password) 
{ println("authenticating $userName.")}

fun main() {
    auth(UserName("user1"), Password("12345"))
    //does not compile due to type mismatch
    auth(Password("12345"), UserName("user1"))
}

參數列表變得愈來愈混亂,而且在調用方來看,編譯器不容許出現不匹配的狀況。先前描述的多是使用內聯類的最多見方案。它們爲您提供了簡單的類型安全的包裝器,而無需引入其餘堆分配。對於這些狀況,應儘量選擇內聯類。可是,內聯類甚至能夠更智能,這將在下一個用例中演示。

無需額外空間

讓咱們考慮一個採用數字字符串並將其解析爲BigDecimal並同時調整其比例的方法:

/**
 * parses string number into BigDecimal with a scale of 2
 */

fun parseNumber(number: String): BigDecimal {
    return number.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun main() {
    println(parseNumber("100.12212"))
}

該代碼很是簡單,能夠很好地工做,可是一個要求多是您須要以某種方式跟蹤用於解析該數字的原始字符串。爲了解決這個問題,您可能會建立一個包裝類型,或者使用現有的Pair類從該函數返回一對值。這些方法雖然顯然會分配額外的空間,但仍然是有效的,在特殊狀況下應避免使用。內聯類能夠幫助您。咱們已經注意到,內聯類不能具備帶有幕後字段的多個屬性。可是,它們能夠具備屬性和函數形式的簡單計算成員。咱們能夠爲咱們的用例建立一個內聯類,該類包裝原始的String並提供按需分析咱們的值的方法或屬性。對於用戶而言,這看起來像是圍繞兩種類型的普通數據包裝器,而在最佳狀況下它不會增長任何運行時開銷

inline class ParsableNumber(val original: String) {
    val parsed: BigDecimal
        get() 
= original.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun getParsableNumber(number: String): ParsableNumber {
    return ParsableNumber(number)
}

fun main() {
    val parsableNumber = getParsableNumber("100.12212")
    println(parsableNumber.parsed)
    println(parsableNumber.original)
}

如您所見,getParsableNumber方法返回咱們內聯類的實例,該實例提供原始(基礎類型)和已分析(計算的已分析數量)兩個屬性。這是一個有趣的用例,值得再次在字節碼級別上觀察:

public final class ParsableNumber {
   @NotNull
   private final String original;

   @NotNull
   public final String getOriginal() return this.original; }

   // $FF: synthetic method
   private ParsableNumber(@NotNull String original) {
      Intrinsics.checkParameterIsNotNull(original, "original");
      super();
      this.original = original;
   }

   @NotNull
   public static final BigDecimal getParsed_impl(String $this) {
      BigDecimal var10000 = (new BigDecimal($this)).setScale(2, RoundingMode.HALF_UP);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "original.toBigDecimal().…(2, RoundingMode.HALF_UP)");
      return var10000;
   }

   @NotNull
   public static String constructor_impl(@NotNull String original) {
      Intrinsics.checkParameterIsNotNull(original, "original");
      return original;
   }

   // $FF: synthetic method
   @NotNull
   public static final ParsableNumber box_impl(@NotNull String v) {
      Intrinsics.checkParameterIsNotNull(v, "v");
      return new ParsableNumber(v);
   }

   // $FF: synthetic method
   @NotNull
   public final String unbox_impl() return this.original; }

    //more Object related implementations
}

生成的包裝類ParsableNumber幾乎相似於前面顯示的WrappedInt類。可是,一個重要的區別是getParsed_impl函數,該函數表示已解析的可計算屬性。如您所見,該函數被實現爲靜態函數,該靜態函數接受字符串並返回BigDecimal。那麼在調用者代碼中如何利用呢?

@NotNull
public static final String getParsableNumber(@NotNull String number) {
    Intrinsics.checkParameterIsNotNull(number, "number");
    return ParsableNumber.constructor_impl(number);
}

public static final void main() {
    String parsableNumber = getParsableNumber("100.12212");
    BigDecimal var1 = ParsableNumber.getParsed_impl(parsableNumber);
    System.out.println(var1);
    System.out.println(parsableNumber);
}

不出所料,getParsableNumber沒有引用咱們的包裝類型。它只是返回String而不引入任何新類型。在主體中,咱們看到靜態的getParsed_impl用於將給定的String解析爲BigDecimal。一樣,不使用ParsableNumber。

縮小擴展函數的範圍

擴展函數的一個常見問題是,若是在諸如String之類的常規類型上進行定義,它們可能會污染您的命名空間。例如,您可能須要一個擴展函數,將JSON字符串轉換爲相應的類型:

inline fun <reified T> String.asJson() = jacksonObjectMapper().readValue<T>(this)

要將給定的字符串轉換爲數據JsonData,您能夠執行如下操做:

val jsonString = """{ "x":200, "y":300 }"""
val data: JsonData = jsonString.asJson()

可是,擴展功能也可用於表示其餘數據的字符串,儘管可能沒有多大意義:

"whatever".asJson<JsonData> //將會失敗

因爲字符串不包含有效的JSON數據,所以此代碼將失敗。咱們該怎麼作才能使上面顯示的擴展名僅適用於某些字符串?不錯,您須要的是內聯類:

縮小擴展範圍

inline class JsonString(val value: String)
inline fun <reified T> JsonString.asJson() 
= jacksonObjectMapper().readValue<T>(this.value)

當咱們引入用於保存JSON數據的字符串的包裝器並相應地將擴展名更改成使用JsonString接收器時,上述問題已獲得解決。該擴展名將再也不出如今任何任意String上,而是僅出如今咱們有意識地包裝在JsonString中的那些字符串上。

無符號類型

當查看版本1.3中添加到語言中的無符號整數類型時,內聯類的另外一個很好的案例就變得顯而易見了,這也是一個實驗功能:

public inline class UInt @PublishedApi internal constructor(@PublishedApi internal val dataInt) : Comparable<UInt>

如您所見,UInt類被定義爲包裝常規的帶符號整數數據的無符號類。

總結

內聯類是一個很棒的工具,可用於減小包裝類型的堆分配,並幫助咱們解決各類問題。可是,請注意,某些狀況(例如將內聯類用做可空類型)會進行裝箱。因爲內聯類仍處於Alpha階段,所以您必須接受將來代碼會因爲其行爲的更改而在未來的版本中失效。這一點咱們要記住。不過,我認爲如今就開始使用它們是有合理的。


推薦閱讀


最強總結 | Java-Kotlin雙譯手冊,收藏必備

最強總結 | 帶你快速搞定kotlin開發(下篇)

最強總結 | 帶你快速搞定kotlin開發(中篇)

最強總結 | 帶你快速搞定kotlin開發(上篇)


掃碼關注我

一塊兒感悟技術魅力

點分享

點收藏

點點贊

點在看

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

相關文章
相關標籤/搜索