1. 程式人生 > 實用技巧 >Kotlin內聯類工作原理及使用案例,看完你會回來謝我的

Kotlin內聯類工作原理及使用案例,看完你會回來謝我的

內聯類非常的簡單,您只需要在類的前面加上inline關鍵字就可以:

inlineclassWrappedInt(valvalue:Int)

內聯類有一些或多或少明顯的限制:需要在主建構函式中精確指定一個屬性,如value所示。您不能在一個內聯類中包裝多個值。內聯類中也禁止包含init塊,並且不能具有帶有幕後欄位的屬性。內聯類可以具有簡單的可計算屬性,但是我們將在本文後面看到。

在執行時,將盡可能使用內聯類的包裝型別而不使用其包裝。這類似於Java的框式型別,例如Integer或Boolean,只要編譯器可以這樣做,它們就會被表示為它們對應的原始型別。這正是Kotlin中內聯類的一大賣點:內聯類時,除非絕對必要,否則類本身不會在位元組碼中使用。內聯類大大減少了執行時的空間開銷。

執行時

在執行時,可以將內聯類表示為包裝型別和基礎型別。如前一段所述,編譯器更喜歡使用內聯類的基礎(包裝)型別來儘可能地優化程式碼。這類似於int和Integer之間的裝箱。但是,在某些情況下,編譯器需要使用包裝器本身,因此它將在編譯期間生成:

publicfinalclassWrappedInt{
privatefinalintvalue;

publicfinalintgetValue(){returnthis.value;}

//$FF:syntheticmethod
privateWrappedInt(intvalue){this.value=value;}

publicstaticintconstructor_impl(intvalue){returnvalue;}

//$FF:syntheticmethod
@NotNull
publicstaticfinalWrappedIntbox_impl(intv){returnnewWrappedInt(v);}

//$FF:syntheticmethod
publicfinalintunbox_impl(){returnthis.value;}

//moreObjectrelatedimplementations
}

此程式碼段顯示了內聯類簡化的Java位元組碼。除了一些顯而易見的東西,例如value欄位及其getter之外,建構函式是私有的,而新物件將通過Constructor_impl建立,該物件實際上並不使用包裝器型別,而僅返回傳入的基礎型別。最後,您可以看到box_impl和unbox_impl函式,可能如您所期望的,它們的目的在於拆裝箱的操作。現在,讓我們看看在程式碼中如何使用內聯類。

使用內聯類

funtake(w:WrappedInt){
println(w.value)
}

funmain(){
valinlined=WrappedInt(5)
take(inlined)
}

在此程式碼段中,正在建立WrappedInt並將其傳遞給列印其包裝值的函式。相應的Java位元組碼,如下所示:

publicstaticfinalvoidtake_hqTGqkw(intw){
System.out.println(w);
}

publicstaticfinalvoidmain(){
intinlined=WrappedInt.constructor_impl(5);
take_hqTGqkw(inlined);
}

在已編譯的程式碼中,沒有建立WrappedInt例項。儘管使用了靜態的builder_impl函式,它只是返回一個int值,然後將其傳遞給take函式,該函式也對我們最初在原始碼中擁有的內聯類的型別一無所知。請注意,接受內聯類引數的函式名稱會用位元組碼中生成的雜湊碼擴充套件。這樣,它們可以與接受基礎型別作為引數的過載函式區分開:

funtake(w:WrappedInt)=println(w.value)
funtake(v:Int)=println(v.value)

為了使這兩種take方法在JVM位元組碼中可用並避免簽名衝突,編譯器將第一個方法重新命名為take-hqTGqkw之類的東西。注意,上面的示例確實顯示了“ _”而不是“-”,因為Java不允許方法名稱包含破折號,這也是為什麼不能從Java呼叫接受內聯類的方法的原因。

內聯類的裝箱

前面我們看到過,box_impl和unbox_impl函式是為內聯類建立的,那麼什麼時候需要它們?Kotlin的文件引用了一條經驗法則:

內聯類在用作其他型別時會被裝箱。

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

inlineclassWrappedInt(valvalue:Int)

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

funmain(){
take(WrappedInt(5))
}

在此程式碼中,我們修改了take函式以採用可為空的WrappedInt,並在引數不為null時顯示基礎型別。

publicstaticfinalvoidtake_G1XIRLQ(@NullableWrappedIntw){
if(Intrinsics.areEqual(w,(Object)null)^true){
intvar1=w.unbox_impl();
System.out.println(var1);
}
}

publicstaticfinalvoidmain(){
take_G1XIRLQ(WrappedInt.box_impl(WrappedInt.constructor_impl(5)));
}

在位元組碼中,take函式現在不再直接接受基礎型別。它必須改為使用裝箱型別。列印其內容時,將呼叫unbox_impl。在呼叫的地方,我們可以看到box_impl用於建立WrappedInt的裝箱例項。

顯然,我們希望儘可能避免裝箱。請記住,內聯類以及原始型別的特定用法通常都依賴於此技術,因此可能必須重新考慮是否該這麼做。

使用案例

我們看到內聯類具有巨大的優勢:在最佳情況下,由於避免了額外的堆分配,它們可以大大減少執行時的開銷。但是我們什麼時候適合使用這種包裝型別呢?

更好的區分型別

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

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

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

auth("12345","user1")

由於這兩個引數均為String型別,因此您可能會弄亂它們的順序,當然,隨著引數數量的增加,這種順序的可能性更大。這些型別的包裝型別可以幫助您減輕這種風險,因此內聯類是一個很棒的工具:

inlineclassPassword(valvalue:String)
inlineclassUserName(valvalue:String)

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

funmain(){
auth(UserName("user1"),Password("12345"))
//doesnotcompileduetotypemismatch
auth(Password("12345"),UserName("user1"))
}

引數列表變得越來越混亂,並且在呼叫方來看,編譯器不允許出現不匹配的情況。先前描述的可能是使用內聯類的最常見方案。它們為您提供了簡單的型別安全的包裝器,而無需引入其他堆分配。對於這些情況,應儘可能選擇內聯類。但是,內聯類甚至可以更智慧,這將在下一個用例中演示。

無需額外空間

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

/**
*parsesstringnumberintoBigDecimalwithascaleof2
*/
funparseNumber(number:String):BigDecimal{
returnnumber.toBigDecimal().setScale(2,RoundingMode.HALF_UP)
}

funmain(){
println(parseNumber("100.12212"))
}

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

inlineclassParsableNumber(valoriginal:String){
valparsed:BigDecimal
get()=original.toBigDecimal().setScale(2,RoundingMode.HALF_UP)
}

fungetParsableNumber(number:String):ParsableNumber{
returnParsableNumber(number)
}

funmain(){
valparsableNumber=getParsableNumber("100.12212")
println(parsableNumber.parsed)
println(parsableNumber.original)
}

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

publicfinalclassParsableNumber{
@NotNull
privatefinalStringoriginal;

@NotNull
publicfinalStringgetOriginal(){returnthis.original;}

//$FF:syntheticmethod
privateParsableNumber(@NotNullStringoriginal){
Intrinsics.checkParameterIsNotNull(original,"original");
super();
this.original=original;
}

@NotNull
publicstaticfinalBigDecimalgetParsed_impl(String$this){
BigDecimalvar10000=(newBigDecimal($this)).setScale(2,RoundingMode.HALF_UP);
Intrinsics.checkExpressionValueIsNotNull(var10000,"original.toBigDecimal().…(2,RoundingMode.HALF_UP)");
returnvar10000;
}

@NotNull
publicstaticStringconstructor_impl(@NotNullStringoriginal){
Intrinsics.checkParameterIsNotNull(original,"original");
returnoriginal;
}

//$FF:syntheticmethod
@NotNull
publicstaticfinalParsableNumberbox_impl(@NotNullStringv){
Intrinsics.checkParameterIsNotNull(v,"v");
returnnewParsableNumber(v);
}

//$FF:syntheticmethod
@NotNull
publicfinalStringunbox_impl(){returnthis.original;}

//moreObjectrelatedimplementations
}

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

@NotNull
publicstaticfinalStringgetParsableNumber(@NotNullStringnumber){
Intrinsics.checkParameterIsNotNull(number,"number");
returnParsableNumber.constructor_impl(number);
}

publicstaticfinalvoidmain(){
StringparsableNumber=getParsableNumber("100.12212");
BigDecimalvar1=ParsableNumber.getParsed_impl(parsableNumber);
System.out.println(var1);
System.out.println(parsableNumber);
}

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

縮小擴充套件函式的範圍

擴充套件函式的一個常見問題是,如果在諸如String之類的常規型別上進行定義,它們可能會汙染您的名稱空間。例如,您可能需要一個擴充套件函式,將JSON字串轉換為相應的型別:

inlinefun<reifiedT>String.asJson()=jacksonObjectMapper().readValue<T>(this)

要將給定的字串轉換為資料JsonData,您可以執行以下操作:

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

但是,擴充套件功能也可用於表示其他資料的字串,儘管可能沒有多大意義:

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

由於字串不包含有效的JSON資料,因此此程式碼將失敗。我們該怎麼做才能使上面顯示的副檔名僅適用於某些字串?不錯,您需要的是內聯類:

縮小擴充套件範圍

inlineclassJsonString(valvalue:String)
inlinefun<reifiedT>JsonString.asJson(www.dongdongrji.cn)=jacksonObjectMapper().readValue<T>(this.value)

當我們引入用於儲存JSON資料的字串的包裝器並相應地將副檔名更改為使用JsonString接收器時,上述問題已得到解決。該副檔名將不再出現在任何任意String上,而是僅出現在我們有意識地包裝在JsonString中的那些字串上。

無符號型別

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

publicinlineclassUIn  t http://www.jintianxuesha.com/ @PublishedApiinternalconstructor(@PublishedApiinternalvaldata:Int):Comparable<UInt>

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

總結

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