簡述: 上接上篇文章,咱們深刻分析了Kotlin1.3版本中的Contract契約的內容,那麼這篇文章將會繼續把Kotlin1.3新特性研究完畢。這篇文章還有個很是重要的點就是inline class 內聯類。關於內聯類的知識除了這篇文章會有介紹,後面立刻會翻譯幾篇有關Kotlin中的內聯類相關內容。只有一個目的完全搞定Kotlin中的內聯類。那咱們一塊兒來看下本次提綱:java
關於inline內聯相信你們都不陌生吧,在實際開發中咱們常常會使用Kotlin中的inline內聯函數。那你們還記得inline內聯做用嗎? 這裏再次複習下inline內聯的做用:python
inline內聯函數主要有兩大做用:數組
用於lambda表達式調用,下降Function系列對象實例建立的內存開銷,從而提升性能。聲明成內聯函數的話,而是在調用的時把調用的方法給替換掉,能夠下降很大的性能開銷。bash
另外一個內聯函數做用就是它能是泛型函數類型實參進行實化,在運行時能拿到類型實參的信息。app
經過複習了inline函數的兩大做用,實際上內聯類存在乎義和inline函數第一個做用有點像。有時候業務場景須要針對某種類型建立包裝器類。 可是,使用包裝器類避免不了去實例化這個包裝器類,可是這樣會帶來額外的建立對象的堆分配,它會引入運行時開銷。 此外,若是包裝類型是基礎類型的,性能損失是很糟糕的,由於基礎類型一般在運行時大大優化,而它們的包裝器沒有獲得任何特殊處理。框架
那究竟是什麼場景會須要內聯類呢? 實際上,在開發中有時候就基本數據類型外加變量名是沒法徹底表達某個字段的含義,甚至還有可能形成歧義,這種場景就特別適合內聯類包裝器。是否是很抽象,那麼一塊兒來看個例子。jvm
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}
複製代碼
咱們仔細分析下joinToString函數,它有不少個函數參數,其中讓人感到歧義就是前面三個: separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = ""
.有的人就會說不會有歧義啊,定義得很清楚很明白了,separator,prefix,postfix形參名明顯都有本身的含義啊。僅僅從joinToString這個函數的角度來看確實比較清晰。但是你有沒有想過外層調用者困惑呢。對於外部調用者前面三個參數都是CharSequence類型,對於他們而言除非去看函數聲明而後才知道每一個參數表明什麼意思,外面調用者很容易把三個參數調用順序弄混了。就像下面這樣。maven
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr("<",",",">"))
//這段代碼在調用者看來就僅僅是傳入三個字符串,給人看起來很迷惑,根本就不知道每一個字符串實參到底表明是什麼意思。
//這裏代碼是很脆弱的,在不看函數聲明時,字符串要麼很容易被打亂順序,要麼沒人敢改這裏的代碼了。
}
fun <T> Iterable<T>.joinToStr(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = ""): String{
return this.joinToString(separator, prefix, postfix)
}
複製代碼
上面那種問題,爲何咱們平時感覺不到呢? 這是由於IDE幫你作了不少工做,可是試想下若是你的代碼離開IDE就忽然感受很醜陋. 就看上面那段代碼假如沒有給你joinToStr函數聲明定義,是否是對傳入三個參數一臉懵逼啊。針對上述實際上有三種解決辦法:ide
第一種: IDE高亮提示函數
第二種: Kotlin中命名參數
關於Kotlin中命名參數解決問題方式和IDE提示解決思路是同樣的。關於Kotlin中命名參數不瞭解的能夠參考我以前這篇文章淺談Kotlin語法篇之如何讓函數更好地調用(三)
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr(prefix = "<", separator = ",", postfix = ">"))
}
複製代碼
第三種: 使用包裝器類解決方案
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr(Speparator(","), Prefix("<"), Postfix(">")))
}
class Speparator(val separator: CharSequence)
class Prefix(val prefix: CharSequence)
class Postfix(val postfix: CharSequence)
fun <T> Iterable<T>.joinToStr( separator: Speparator, prefix: Prefix, postfix: Postfix ): String {
return this.joinToString(separator.separator, prefix.prefix, postfix.postfix)
}
複製代碼
看到這裏是否是不少人以爲這樣實現有問題,雖然它能很好解決咱們上述類型不明確的問題。可是卻引入一個更大問題,須要額外建立Speparator、Prefix、Postfix實例對象,帶來不少的內存開銷。從投入產出比來看,估計沒人這麼玩吧。這是由於inline class沒出來以前,可是若是inline class能把性能開銷下降到和直接使用String同樣的性能開銷,你還認爲它是不好的方案嗎? 請接着往下看
第四種: Kotlin中inline class終極解決方案
針對上述問題,Kotlin提出inline class解決方案,就是從源頭上解決問題。一塊兒來看看:
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr(Speparator(","), Prefix("<"), Postfix(">")))
}
//相比上一個方案,僅僅是多加了inline關鍵字
inline class Speparator(val separator: CharSequence)
inline class Prefix(val prefix: CharSequence)
inline class Postfix(val postfix: CharSequence)
fun <T> Iterable<T>.joinToStr( separator: Speparator, prefix: Prefix, postfix: Postfix ): String {
return this.joinToString(separator.separator, prefix.prefix, postfix.postfix)
}
複製代碼
經過使用inline class來改造這個案例,會發現剛剛上述那個問題就被完全解決了,外部調用者不會再一臉懵逼了,一看就很明確,是該傳入Speparator、Prefix、Postfix對象。性能開銷問題的徹底不用擔憂了,它和直接使用String的性能幾乎是同樣的,這樣一來是否是以爲這種解決方案還不錯呢。至於它是如何作到的,請接着往下看。這就是爲何須要inline class場景了。
inline class 爲了解決包裝器類帶來額外性能開銷問題的一種特殊類。
基本結構很簡單就是在普通class前面加上inline關鍵字
由於kotlin中的inline class仍是處於Experimental中,因此你要使用它須要作一些額外的配置。首先你的Kotlin Plugin升級到1.3版以上,而後配置gradle,這裏給出IntelliJ IDEA和AndroidStudio嚐鮮gradle配置:
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions {
freeCompilerArgs = ["-XXLanguage:+InlineClasses"]
}
}
複製代碼
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs = ['-XXLanguage:+InlineClasses']
}
}
複製代碼
<configuration>
<args>
<arg>-XXLanguage:+InlineClasses</arg>
</args>
</configuration>
複製代碼
估計不少人都沒使用過typealias吧,若是還不瞭解typealias的話請參考我以前的這篇文章:[譯]有關Kotlin類型別名(typealias)你須要知道的一切. 其實上述那個問題還能夠用typealias來改寫,可是你會發現是有點缺陷的,並無達到想要效果。能夠給你們看下:
typealias Speparator = CharSequence
typealias Prefix = CharSequence
typealias Postfix = CharSequence
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr(",", "<", ">"))
}
fun <T> Iterable<T>.joinToStr( separator: Speparator, prefix: Prefix, postfix: Postfix ): String {
return this.joinToString(separator, prefix, postfix)
}
複製代碼
關於inline class和typealias有很大相同點不一樣點,相同點在於: 他們二者看起來貌似都引入一種新類型,而且二者都將在運行時表現爲基礎類型。不一樣點在於: typealias僅僅是給基礎類型取了一個別名而已,而inline class是基礎類型一個包裝器類。換句話說inline class纔是真正引入了一個新的類型,而typealias則沒有。
是否是仍是有點抽象啊,來個例子你就明白了
typealias Token = String
inline class TokenWrapper(val value: String)
fun main(args: Array<String>) {
val token: Token = "r3huae03zdhreol38fdjhkdfd8df"//能夠看出這裏Token名稱徹底是當作String類型來用了,至關於給String取了一個有意義的名字
val tokenWrapper = TokenWrapper("r3huae03zdhreol38fdjhkdfd8df")//而inline class則是把String類型的值包裹起來,至關於String的一個包裝器類。
println("token is $token")
println("token value is ${tokenWrapper.value}")//這裏tokenWrapper並不能像token同樣當作String來使用,而是須要打開包裝器取裏面value值
}
複製代碼
經過反編譯分析,就能清楚明白爲何inline class不會存在建立對象性能開銷。實際上inline class在運行時表現和直接使用基礎類型的效果是同樣的。
就拿上述例子反編譯分析一下:
TokenWrapper類
public final class TokenWrapper {
@NotNull
private final String value;
@NotNull
public final String getValue() {//這個方法就不用說了吧,val自動生成的get方法
return this.value;
}
// $FF: synthetic method
private TokenWrapper(@NotNull String value) {//構造器私有化
Intrinsics.checkParameterIsNotNull(value, "value");
super();
this.value = value;
}
@NotNull
public static String constructor_impl/* $FF was: constructor-impl*/(@NotNull String value) {
Intrinsics.checkParameterIsNotNull(value, "value");
return value;
}
// $FF: synthetic method
@NotNull
public static final TokenWrapper box_impl/* $FF was: box-impl*/(@NotNull String v) {//box-impl裝箱操做
Intrinsics.checkParameterIsNotNull(v, "v");
return new TokenWrapper(v);
}
@NotNull
public static String toString_impl/* $FF was: toString-impl*/(String var0) {//toString方法實現
return "TokenWrapper(value=" + var0 + ")";
}
public static int hashCode_impl/* $FF was: hashCode-impl*/(String var0) {//hashCode方法實現
return var0 != null ? var0.hashCode() : 0;
}
public static boolean equals_impl/* $FF was: equals-impl*/(String var0, @Nullable Object var1) {//equals方法實現
if (var1 instanceof TokenWrapper) {
String var2 = ((TokenWrapper)var1).unbox-impl();
if (Intrinsics.areEqual(var0, var2)) {
return true;
}
}
return false;
}
public static final boolean equals_impl0/* $FF was: equals-impl0*/(@NotNull String p1, @NotNull String p2) {
Intrinsics.checkParameterIsNotNull(p1, "p1");
Intrinsics.checkParameterIsNotNull(p2, "p2");
throw null;
}
// $FF: synthetic method
@NotNull
public final String unbox_impl/* $FF was: unbox-impl*/() {//拆箱操做
return this.value;
}
public String toString() {
return toString-impl(this.value);//委託給對應靜態方法實現
}
public int hashCode() {
return hashCode-impl(this.value);
}
public boolean equals(Object var1) {
return equals-impl(this.value, var1);
}
}
複製代碼
能夠看到TokenWrapper類中反編譯後的源碼重寫了Any中toString、equal、hashCode三個方法。而後這三個方法又委託到給外部定義對應的靜態方法來實現。unbox_impl和box_impl兩個函數實際上就是拆箱和裝箱的操做
main函數
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String token = "r3huae03zdhreol38fdjhkdfd8df";//能夠看到typealias定義的Token名字已經消失的無影無蹤,只剩下String基礎類型。
String tokenWrapper = TokenWrapper.constructor-impl("r3huae03zdhreol38fdjhkdfd8df");//TokenWrapper類痕跡依然存在
String var3 = "token is " + token;
System.out.println(var3);
var3 = "token value is " + tokenWrapper;
System.out.println(var3);
}
複製代碼
分析以下: 能夠先從main函數入手,重點看這行:
String tokenWrapper = TokenWrapper.constructor-impl("r3huae03zdhreol38fdjhkdfd8df");
複製代碼
而後再跳到TokenWrapper中constructor-impl方法
@NotNull
public static String constructor_impl/* $FF was: constructor-impl*/(@NotNull String value) {
Intrinsics.checkParameterIsNotNull(value, "value");
return value;//這裏僅僅是接收一個value值,作了一個參數檢查,最後就直接把這個value又返回出去了。
}
複製代碼
因此main函數中的val tokenWrapper = TokenWrapper("r3huae03zdhreol38fdjhkdfd8df")
在運行時至關於val tokenWrapper: String = "r3huae03zdhreol38fdjhkdfd8df"
. 因此性能問題就不用擔憂了。
Kotlin1.3新特性對when表達式作一個寫法上的優化,爲何這麼說呢?僅僅就是寫法上的優化,實際上什麼都沒作,一塊兒來研究下。不知道你們在使用when表達式有沒有這樣感覺(反正我是有過這樣的感覺): 在when表達式做用域內,老天啊請賜我一個像lambda表達式中的同樣it實例對象指代吧。---來自衆多Kotlin開發者心聲。一塊兒看下這個例子:
@JvmStatic
private fun fillIntentArguments(intent: Intent, params: Array<out Pair<String, Any?>>) {
params.forEach {
val value = it.second//因爲沒有像lamba那樣的it指代只能在when表達式最外層定義一個局部變量value,以便於在when表達式體內使用value.
when (value) {
null -> intent.putExtra(it.first, null as Serializable?)
is Int -> intent.putExtra(it.first, value)//能夠看到這裏,若是value能像lambda表達式中it指代該多好,能夠沒有
is Long -> intent.putExtra(it.first, value)
is CharSequence -> intent.putExtra(it.first, value)
is String -> intent.putExtra(it.first, value)
is Float -> intent.putExtra(it.first, value)
is Double -> intent.putExtra(it.first, value)
...
}
return@forEach
}
}
複製代碼
能夠看到上面的1.3版本以前源碼案例實現,本就一個when表達式的實現因爲在表達式內部須要使用傳入值,可是呢表達式做用域內又不能像lambda表達式內部那樣快樂使用it,因此被活生生拆成兩行代碼實現,是否是很鬱悶。關於這個問題,官方已經注意到了,能夠看到Kotlin團隊的大佬們對開發者的問題處理仍是蠻積極的,立刻就優化這個問題。
官方究竟是怎麼優化的呢? 那麼有的人就說了是否是像lambda表達式同樣賜予咱們一個it指代呢。官方的回答是: NO. 一塊兒再來看1.3版本的實現:
private fun fillIntentArguments(intent: Intent, params: Array<out Pair<String, Any?>>) {
params.forEach {
when (val value = it.second) {//看到沒有,官方說你不是想要一個when表達式實現嗎,那行把value縮進來了. 這樣在when表達式內部快樂使用value了
null -> intent.putExtra(it.first, null as Serializable?)
is Int -> intent.putExtra(it.first, value)
is Long -> intent.putExtra(it.first, value)
is CharSequence -> intent.putExtra(it.first, value)
is String -> intent.putExtra(it.first, value)
is Float -> intent.putExtra(it.first, value)
is Double -> intent.putExtra(it.first, value)
...
}
return@forEach
}
}
複製代碼
fun main(args: Array<String>) {
val value = getValue()
when (value) {
is Int -> "This is Int Type, value is $value".apply(::println)
is String -> "This is String Type, value is $value".apply(::println)
is Double -> "This is Double Type, value is $value".apply(::println)
is Float -> "This is Float Type, value is $value".apply(::println)
else -> "unknown type".apply(::println)
}
}
fun getValue(): Any {
return 100F
}
複製代碼
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Object value = getValue();
String var3;
if (value instanceof Integer) {
var3 = "This is Int Type, value is " + value;
System.out.println(var3);
} else if (value instanceof String) {
var3 = "This is String Type, value is " + value;
System.out.println(var3);
} else if (value instanceof Double) {
var3 = "This is Double Type, value is " + value;
System.out.println(var3);
} else if (value instanceof Float) {
var3 = "This is Float Type, value is " + value;
System.out.println(var3);
} else {
var3 = "unknown type";
System.out.println(var3);
}
}
@NotNull
public static final Object getValue() {
return 100.0F;
}
複製代碼
fun main(args: Array<String>) {
when (val value = getValue()) {//when表達式條件直接是一個表達式,並用value保存了返回值
is Int -> "This is Int Type, value is $value".apply(::println)
is String -> "This is String Type, value is $value".apply(::println)
is Double -> "This is Double Type, value is $value".apply(::println)
is Float -> "This is Float Type, value is $value".apply(::println)
else -> "unknown type".apply(::println)
}
}
fun getValue(): Any {
return 100F
}
複製代碼
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Object value = getValue();
String var2;
if (value instanceof Integer) {
var2 = "This is Int Type, value is " + value;
System.out.println(var2);
} else if (value instanceof String) {
var2 = "This is String Type, value is " + value;
System.out.println(var2);
} else if (value instanceof Double) {
var2 = "This is Double Type, value is " + value;
System.out.println(var2);
} else if (value instanceof Float) {
var2 = "This is Float Type, value is " + value;
System.out.println(var2);
} else {
var2 = "unknown type";
System.out.println(var2);
}
}
@NotNull
public static final Object getValue() {
return 100.0F;
}
複製代碼
經過對比二者實現方式反編譯的代碼你會發現沒有任何變化,因此這就是我說爲何實際上沒作什麼操做。
還記得開發者日大會上官方佈道師Hali在講Kotlin 1.3新特性的時候,第一個例子就是講無參數main函數,在他認爲這是一件很興奮的事。下面給出官方一張動圖一塊兒興奮一下:
不知道你們在開發中有沒有被其餘動態語言開發的人吐槽過。好比最簡單的在程序中打印一行內容的時候,靜態語言就比較繁瑣用Java舉例先得定義一個類,而後再定義main函數,函數中還得傳入數組參數。人家python一行print代碼就解決了。其實Kotlin以前版本相對Java仍是比較簡單至少不須要定義類了,可是Kotlin 1.3就直接把main函數中的參數幹掉了(注意: 這裏指的是帶參數和不帶參數共存,並非徹底把帶參main函數給替換掉了)。
能夠你們有沒有思考過無參main函數是怎麼實現的呢? 不妨咱們一塊兒來探索一波,來了你就懂了,很簡單。 來個Hello Kotlin的例子哈。
fun main(){
println("Hello Kotlin")
}
複製代碼
將上述代碼反編譯成Java代碼以下
public final class NewMainKt {
public static final void main() {//外部定義無參的main函數
String var0 = "Hello Kotlin";
System.out.println(var0);
}
// $FF: synthetic method
public static void main(String[] var0) {//自動生成一個帶參數的main函數
main();//而後再去調用一個無參的main函數
}
}
複製代碼
看完反編譯後的Java代碼是否是一眼就清楚,所謂的無參main函數,實際上就是個障眼法。默認生成一個帶參數的main函數繼續做爲執行的入口,只不過在這帶參數的main函數中再去調用外部無參main函數。
注意: 使用無參main函數有好處也有不妥的地方,好處顯而易見的是使用很是簡潔。可是也就間接喪失了main函數執行入口配置參數功能。因此官方並無把帶參數main函數去掉,而是共存。兩種main函數都是有各自使用場景的。
咱們天然而然知道在類的伴生對象是徹底支持@JvmStatic,@JvmField註解。首先呢,關於@JvmStatic,@JvmField註解我想有必要說明下它們的做用。
他們做用主要是爲了在Kotlin伴生對象中定義的一個函數或屬性,可以在Java中像調用靜態函數和靜態屬性那樣類名.函數名/屬性名方式調用,讓Java開發者徹底沒法感知這是一個來自Kotlin伴生對象中的函數或屬性。若是不加註解那麼在Java中調用方式就是類名.Companion.函數名/屬性名。你讓一個Java開發者知道Companion存在,只會讓他一臉懵逼。
這就意味着1.3接口中伴生對象中函數和屬性能夠向類中同樣快樂地使用@JvmStatic,@JvmField註解了。 一塊兒來看個使用例子:
//在Kotlin接口中定義
interface Foo {
companion object {
@JvmField
val answer: Int = 42
@JvmStatic
fun sayHello() {
println("Hello, world!")
}
}
}
//在Java代碼中調用
class TestFoo {
public static void main(String[] args){
System.out.println("Foo test: " + Foo.answer + " say: " + Foo.sayHello());
}
}
複製代碼
不知道你們是否還記得我以前幾篇文章深刻研究過Lambda表達式整個運行原理,其中就詳細講了關於Function系列的接口。由於咱們知道Lambda表達式最後會編譯成一個class類,這個類會去繼承Kotlin中Lambda的抽象類(在kotlin.jvm.internal包中)而且實現一個Function0...FunctionN(在kotlin.jvm.functions包中)的接口(這個N是根據lambda表達式傳入參數的個數決定的,目前接口N的取值爲 0 <= N <= 22,也就是lambda表達式中函數傳入的參數最多也只能是22個),這個Lambda抽象類是實現了FunctionBase接口,該接口中有兩個方法一個是getArity()獲取lambda參數的元數,toString()實際上就是打印出Lambda表達式類型字符串,獲取Lambda表達式類型字符串是經過Java中Reflection類反射來實現的。FunctionBase接口繼承了Function,Serializable接口。(具體詳細內容請參考個人這篇文章: 淺談Kotlin語法篇之lambda編譯成字節碼過程徹底解析(七))
由上面分析獲得N取值範圍是0 <= N <= 22,要是此時有個lambda表達式函數參數個數是23個也就是大於22個時候該怎麼辦?不就玩不轉了嗎? 雖然大於22個參數場景不多不多,可是這始終算是一個缺陷。因此此次Kotlin 1.3直接就完全抹平這個缺陷,增長了FunctionN接口支持傳入的是可變長參數列表,也就是支持任意個數參數,這樣擴展性就更強了。
//官方源碼定義
interface FunctionN<out R> : Function<R>, FunctionBase<R> {
/** * Invokes the function with the specified arguments. * * Must **throw exception** if the length of passed [args] is not equal to the parameter count returned by [arity]. * * @param args arguments to the function */
operator fun invoke(vararg args: Any?): R//能夠看到這裏接收是一個vararg 可變長參數,支持任意個數的lambda表達式參數,不再用擔憂超過22個參數該怎麼辦了。
/** * Returns the number of arguments that must be passed to this function. */
override val arity: Int
}
//使用例子僞代碼
fun trueEnterpriseComesToKotlin(block: (Any, Any, ... /* 42 more */, Any) -> Any) {
block(Any(), Any(), ..., Any())
}
複製代碼
在Kotlin 1.3中,註解類能夠嵌套註解類、接口以及伴生對象.關於Kotlin中的註解和反射尚未詳細深刻研究過,這個暫且放一放,等到研究註解時候,會再次探討有關注解類嵌套的問題。
annotation class Foo {
enum class Direction { UP, DOWN, LEFT, RIGHT }
annotation class Bar
companion object {
fun foo(): Int = 42
val bar: Int = 42
}
}
複製代碼
到這裏Kotlin1.3新特性相關的內容就結束。下面將會繼續深刻研究下Kotlin 1.3中的inline class(主要是以翻譯國外優秀文章爲主)。而後就是去深刻研究你們一直期待的協程和ktor框架,並把最終研究成果以文章的形式共享給你們。歡迎關注,會一直持續更新下去~~~
原創系列:
翻譯系列:
實戰系列:
歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~