您好,登錄后才能下訂單哦!
本篇內容主要講解“Kotlin內聯類工作原理是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Kotlin內聯類工作原理是什么”吧!
內聯類非常的簡單,您只需要在類的前面加上inline關鍵字就可以:
inline class WrappedInt(val value: Int)
內聯類有一些或多或少明顯的限制:需要在主構造函數中精確指定一個屬性,如value所示。您不能在一個內聯類中包裝多個值。內聯類中也禁止包含init塊,并且不能具有帶有幕后字段的屬性。內聯類可以具有簡單的可計算屬性,但是我們將在本文后面看到。
在運行時,將盡可能使用內聯類的包裝類型而不使用其包裝。這類似于Java的框式類型,例如Integer或Boolean,只要編譯器可以這樣做,它們就會被表示為它們對應的原始類型。這正是Kotlin中內聯類的一大賣點:內聯類時,除非絕對必要,否則類本身不會在字節碼中使用。內聯類大大減少了運行時的空間開銷。
在運行時,可以將內聯類表示為包裝類型和基礎類型。如前一段所述,編譯器更喜歡使用內聯類的基礎(包裝)類型來盡可能地優化代碼。這類似于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字節碼,如下所示:
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的文檔引用了一條經驗法則:
內聯類在用作其他類型時會被裝箱。
例如,當您將內聯類用作通用類型或可為空的類型時,就會發生裝箱:
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 data: Int) : Comparable<UInt>
如您所見,UInt類被定義為包裝常規的帶符號整數數據的無符號類。
到此,相信大家對“Kotlin內聯類工作原理是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。