Jetbrains开发者日见闻(三)之Kotlin1.3新特性(inline class篇)

2,332

简述: 上接上篇文章,我们深入分析了Kotlin1.3版本中的Contract契约的内容,那么这篇文章将会继续把Kotlin1.3新特性研究完毕。这篇文章还有个非常重要的点就是inline class 内联类。关于内联类的知识除了这篇文章会有介绍,后面马上会翻译几篇有关Kotlin中的内联类相关内容。只有一个目的彻底搞定Kotlin中的内联类。那我们一起来看下本次提纲:

一、inline class内联类(Experimental)

1、复习inline内联函数的作用

关于inline内联相信大家都不陌生吧,在实际开发中我们经常会使用Kotlin中的inline内联函数。那大家还记得inline内联作用吗? 这里再次复习下inline内联的作用:

inline内联函数主要有两大作用:

  • 用于lambda表达式调用,降低Function系列对象实例创建的内存开销,从而提高性能。声明成内联函数的话,而是在调用的时把调用的方法给替换掉,可以降低很大的性能开销。

  • 另一个内联函数作用就是它能是泛型函数类型实参进行实化,在运行时能拿到类型实参的信息。

2、为何需要内联类

通过复习了inline函数的两大作用,实际上内联类存在意义和inline函数第一个作用有点像。有时候业务场景需要针对某种类型创建包装器类。 但是,使用包装器类避免不了去实例化这个包装器类,但是这样会带来额外的创建对象的堆分配,它会引入运行时开销。 此外,如果包装类型是基础类型的,性能损失是很糟糕的,因为基础类型通常在运行时大大优化,而它们的包装器没有得到任何特殊处理。

那到底是什么场景会需要内联类呢? 实际上,在开发中有时候就基本数据类型外加变量名是无法完全表达某个字段的含义,甚至还有可能造成歧义,这种场景就特别适合内联类包装器。是不是很抽象,那么一起来看个例子。

  • 案例一: Iterable扩展函数joinToString源码分析
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类型,对于他们而言除非去看函数声明然后才知道每个参数代表什么意思,外面调用者很容易把三个参数调用顺序弄混了。就像下面这样。

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高亮提示

第二种: 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场景了。

3、内联类的基本介绍

  • 基本定义

inline class 为了解决包装器类带来额外性能开销问题的一种特殊类。

  • 基本结构

基本结构很简单就是在普通class前面加上inline关键字

4、如何尝鲜inline class

因为kotlin中的inline class还是处于Experimental中,所以你要使用它需要做一些额外的配置。首先你的Kotlin Plugin升级到1.3版以上,然后配置gradle,这里给出IntelliJ IDEA和AndroidStudio尝鲜gradle配置:

  • IntelliJ IDEA中gradle配置
compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
    kotlinOptions {
        freeCompilerArgs = ["-XXLanguage:+InlineClasses"]
    }
}
  • AndroidStudio中gradle配置
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions {
        jvmTarget = '1.8'
        freeCompilerArgs = ['-XXLanguage:+InlineClasses']
    }
}
  • 使用maven的配置
 <configuration>
                <args>
                    <arg>-XXLanguage:+InlineClasses</arg>
                </args>
 </configuration>

5、内联类和typealias的区别

估计很多人都没使用过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值
}

6、内联类的反编译源码分析

通过反编译分析,就能清楚明白为什么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". 所以性能问题就不用担心了。

7、内联类使用限制

  • 1、内联类必须含有主构造器且构造器内参数个数有且仅有一个,形参只能是只读的(val修饰)。
  • 2、内联类不能含有init block
  • 3、内联类不能含有inner class

二、when表达式的使用优化

Kotlin1.3新特性对when表达式做一个写法上的优化,为什么这么说呢?仅仅就是写法上的优化,实际上什么都没做,一起来研究下。不知道大家在使用when表达式有没有这样感受(反正我是有过这样的感受): 在when表达式作用域内,老天啊请赐我一个像lambda表达式中的一样it实例对象指代吧。---来自众多Kotlin开发者心声。一起看下这个例子:

1、Anko库源码中fillIntentArguments函数部分代码分析

  @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团队的大佬们对开发者的问题处理还是蛮积极的,马上就优化这个问题。

2、1.3版本when表达式优化版本

官方到底是怎么优化的呢? 那么有的人就说了是不是像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
    }
}

3、优化之后反编译代码对比

  • kotlin 1.3版本之前when表达式实现
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
}
  • kotlin 1.3版本之前when表达式使用反编译代码
  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;
   }
  • kotlin 1.3版本when表达式实现
  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
}
  • kotlin 1.3版本when表达式使用反编译代码
     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;
   }

通过对比两者实现方式反编译的代码你会发现没有任何变化,所以这就是我说为什么实际上没做什么操作。

三、无参的main函数

还记得开发者日大会上官方布道师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注解。首先呢,关于@JvmStatic,@JvmField注解我想有必要说明下它们的作用。

  • @JvmStatic,@JvmField的作用(实际上以前文章中有提到过)

他们作用主要是为了在Kotlin伴生对象中定义的一个函数或属性,能够在Java中像调用静态函数和静态属性那样类名.函数名/属性名方式调用,让Java开发者完全无法感知这是一个来自Kotlin伴生对象中的函数或属性。如果不加注解那么在Java中调用方式就是类名.Companion.函数名/属性名。你让一个Java开发者知道Companion存在,只会让他一脸懵逼。

  • Kotlin 1.3版本接口(interface)中伴生对象支持@JvmStatic,@JvmField

这就意味着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());
    }
}

五、支持可变参数的FunctionN接口

不知道大家是否还记得我之前几篇文章深入研究过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国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~