Kotlin真香系列第五弹:类型进阶

目录

写在前面

一、类的构造器

1.1、主构造器

1.2、副构造器

1.3、同名工厂函数

二、类和成员的可见性

三、类属性的延迟初始化

3.1、为什么要延迟初始化?

3.2、解决方案

四、代理Delegate

4.1、什么是代理

4.2、接口代理

4.3、属性代理

五、单例类

六、内部类

七、数据类

7.1、data class的定义

7.2、Java Bean和data class比较

7.3、如何合理使用data class

7.4、如何作为Java Bean使用

八、枚举类

九、密封类

十、内联类


写在前面

各位小伙伴们早上好啊,上一篇中对Kotlin中的函数做了一些进阶的介绍——《Kotlin真香系列第四弹:函数进阶》,今天继续来说Kotlin,咱们来说说关于Kotlin类相关的一些进阶知识点,都坐稳了,开始发车了!(☆_☆)/~~

一、类的构造器

1.1、主构造器

①、init块

关于Kotlin中类的主构造器其实在之前介绍基本语法的时候咱们已经见过了,最简单的方式就是直接写在类的声明的地方:

这个constructor关键字可以省略,然后写成这个样子:

Kotlin真香系列第五弹:类型进阶_第1张图片

构造器的参数前面同时加了var或者val关键字之后,这就表明不仅定义了构造器的参数也定义了类的属性,当然你可以把var去掉,那么就表明这是一个普通的构造器参数:

构造器参数中如果加了var,实际上这是属性,那么它是类内部全局可见的,甚至如果你定义成了public的,类的外部也是可见的,对于没加var关键字的,实际上就是一个局部变量,只能在构造器内部可见,或者是init块内部可见:

Kotlin真香系列第五弹:类型进阶_第2张图片

那什么是init块呢?init块实际上就是构造器的函数体,可以把它理解成主构造器的方法体:

Kotlin真香系列第五弹:类型进阶_第3张图片

并且init块可以定义多个,类似于Java中的构造块,但是Java中的构造块不能访问到构造器中的参数,而Kotlin的init块可以访问到构造器中的参数,这一点需要注意。Kotlin中的init块虽然可以定义多个,但实际上它们在编译完成之后是会合并到一起执行的:

Kotlin真香系列第五弹:类型进阶_第4张图片   =》  Kotlin真香系列第五弹:类型进阶_第5张图片

②、属性初始化

构造器的写法咱们说完了,看着是挺简单的,但是它内部究竟做了哪些工作呢?构造器嘛顾名思义就是构造,所以它内部就是创建这些成员并且要初始化,如果不初始化会有什么问题呢?如下图所示,不好意思,编译器不会给你通过,会给你报一个错:属性必须初始化或者定义成abstract的,交给子类去初始化,所以意味着所有的属性都需要初始化:

Kotlin真香系列第五弹:类型进阶_第6张图片

Java中如果你不初始化它会默认帮你初始化为null,但是Kotlin中直接初始化是不可以的,因为你必须是可空类型,所以在Kotlin中初始化代码必须由开发者自己来写,必须能够清楚的认识到创建一个类的时候如何维护好属性的生命周期,即:属性的生命周期应该与类的生命周期保持一致:

Kotlin真香系列第五弹:类型进阶_第7张图片

③、类的继承

类的继承这个概念在Java中已经非常熟悉了,在Kotlin中写抽象类之前也已经说过了,在这里再回忆一下,就是使用abstract关键字标记抽象类,子类如果需要继承父类就直接在类定义的最后使用":父类()"即可,这个括号是调用了父类的构造器:

Kotlin真香系列第五弹:类型进阶_第8张图片

1.2、副构造器

在Java里面是没有副构造器这个概念的,但是咱们见过一个类有多个构造方法的这种写法,多个构造器就意味着这个类的创建路径有多条。Kotlin中写在类声明的这一行的构造器叫做主构造器,在类的内部定义的这些构造器就是副构造器,副构造器必须调用主构造器,如下图中this(age,"unknown"),除了在这里调用主构造器以外,也可以调用其它调用了主构造器的副构造器,也就是要保证这个类的构造路径是唯一的:

Kotlin真香系列第五弹:类型进阶_第9张图片

不推荐:不定义主构造器,不定义主构造器当然也是可以的,但是不推荐这种写法,就是你定义了多个副构造器嘛,然后在副构造器上调用了父类的构造器,如果父类只有一个默认的无参构造,此时也可以省略不写:

Kotlin真香系列第五弹:类型进阶_第10张图片 Kotlin真香系列第五弹:类型进阶_第11张图片

推荐:主构造器默认参数,对于一些需要重载的情况,大多数都是可以通过默认参数来解决的,也就是比较推荐主构造器+默认参数的方式去构造类:

如果重载的方法你希望Java代码能看到,可以在函数上面加上@JvmOverloads注解:

Kotlin真香系列第五弹:类型进阶_第12张图片

1.3、同名工厂函数

除了使用构造器构造一个类,还经常会提到的就是使用工厂函数,并且工厂函数可以自定义名字,这一点不像构造器,构造器只能跟类名一样,工厂函数你就可以根据实际场景命名,更加容易理解你的设计意图。你还可以定义跟类名相同的函数,比如下图中:左侧定义了一个Person类,右侧定义了一个函数Person:

Kotlin真香系列第五弹:类型进阶_第13张图片 Kotlin真香系列第五弹:类型进阶_第14张图片

采用了跟类名一样的这种工厂函数的写法的类有一个大家都见过的:String,如下图所示:上面的是通过构造器创建的,下面的实际上调用的是一个函数,你仔细看会发现IDE已经告诉你这俩玩意不一样了,下面的是斜体:

Kotlin真香系列第五弹:类型进阶_第15张图片

二、类和成员的可见性

关于可见性的问题,在之前的代码中可能或多或少的也都提到过,最主要的是,这玩意咱们在Java中是学过的,所以没啥难度,只是可能会稍有区别,理解记忆一下即可:

①、可见性类型

Kotlin真香系列第五弹:类型进阶_第16张图片

②、修饰范围

Kotlin真香系列第五弹:类型进阶_第17张图片

③、internal

首先来看模块的概念:

Kotlin真香系列第五弹:类型进阶_第18张图片

直观的讲,大致可以认为是一个Jar包、一个aar。

internal和default的对比:

Kotlin真香系列第五弹:类型进阶_第19张图片

④、构造器的可见性

如果要声明构造器的可见性,那么需要把constructor关键字写出来:

Kotlin真香系列第五弹:类型进阶_第20张图片

⑤、属性的可见性

Kotlin真香系列第五弹:类型进阶_第21张图片

内部定义的属性同样可以声明可见性:

Kotlin真香系列第五弹:类型进阶_第22张图片

属性也可以单独给它的setter方法声明可见性,但是getter方法不可以随意声明可见性,getter的可见性必须与属性可见性一致:

Kotlin真香系列第五弹:类型进阶_第23张图片  Kotlin真香系列第五弹:类型进阶_第24张图片

同时setter的可见性不能大于属性的可见性:

Kotlin真香系列第五弹:类型进阶_第25张图片

⑥、顶级声明的可见性

Kotlin真香系列第五弹:类型进阶_第26张图片

三、类属性的延迟初始化

3.1、为什么要延迟初始化?

  • 类属性必须在构造时初始化,Kotlin构造器的要求,不初始化会报错
  • 实际应用中,某些成员只有在类构造之后才会被初始化

那这种情况下该怎么办呢?

举个例子:以Android的UI为例,我们都知道,Android的UI需要绑定布局,布局中的组件需要在类中声明比如Activity中,然后定义各个控件,这些控件只有在onCreate()中才能获取,因为我们需要在绑定布局之后才能初始化控件,Java代码就是下面这种:

Kotlin真香系列第五弹:类型进阶_第27张图片

这种情况在Kotlin中怎么写呢?注意onCreate不是构造器哦,直接翻译成Kotlin代码会报错的,因为nameView编译器会要求我们进行初始化,此时该怎么办呢?

3.2、解决方案

①、初始化为null

Kotlin真香系列第五弹:类型进阶_第28张图片

这种方案虽然可以,但是在实际使用的时候还是很恶心的,每次访问nameView的时候都要使用?.或者做一次判空,因此不推荐。

②、使用lateinit

Kotlin真香系列第五弹:类型进阶_第29张图片

lateinit是官方考虑到这种情况,给咱们加的关键字,这种情况要求成员必须声明为var,因为后面需要重新给它赋值。这种方案实际上使用的时候你会发现它也有坑,因为nameView一旦初始化之后之后就不会再修改了,因此容易导致一些潜在的问题。

Kotlin真香系列第五弹:类型进阶_第30张图片

在Kotlin1.2之后,lateinit引入了一个方法,取nameView的引用然后调用isInitialized来判断nameView是否已经被初始化,这种写法看着很奇怪,它跟第一种判断可空类型有啥区别呢?并且这种方式在写法上还将问题给掩盖了,还没有第一种方式显得直观,所以也不推荐使用这种方式。

lateinit的注意事项:

Kotlin真香系列第五弹:类型进阶_第31张图片

③、使用lazy

如果真的需要延迟初始化,可以使用lazy,by也是关键字它是使用了属性代理,这种方案只有在nameView首次被访问的时候才会执行,既可以定义属性,同时也解决了初始化的问题,还有就是延迟获取值,并且它还把声明和初始化放在了一起,很容易定位代码:

④、延迟初始化方案对比

Kotlin真香系列第五弹:类型进阶_第32张图片

四、代理Delegate

4.1、什么是代理

Kotlin真香系列第五弹:类型进阶_第33张图片

4.2、接口代理

首先定义一个接口Api,里面有三个方法a()、b()、c(),然后定义一个类ApiImpl实现Api,那么同时就需要实现里面的三个方法:

Kotlin真香系列第五弹:类型进阶_第34张图片

接着又定义一个类ApiWrapper,它可能会做一些功能的拓展,比如打日志等等,在这里面创建了一个api的属性来包装另外的api实例,这种情况你可能只是想在a()方法中打印日志,结果b()和c()都要实现,是不是有点烦:

Kotlin真香系列第五弹:类型进阶_第35张图片

针对这种情况有没有什么简单的方法呢?当然有啦,直接使用接口代理就OK啦,如下图所示:

Kotlin真香系列第五弹:类型进阶_第36张图片

此时,a()和b()编译器自动帮我们生成,生成的代码就是刚刚咱们看到的那样的,c()的话自己实现一下就好了。

所以这样一来就很方便了,对象api代替类ApiWrapper实现接口Api:

Kotlin真香系列第五弹:类型进阶_第37张图片

接口代理还是比较简单的,这一块用的也比较少,用的更多的还是属性代理。

4.3、属性代理

①、lazy

前面介绍了lazy,它代理的是firstName的getter()方法,第一次调用的时候会把lambda表达式里面的值计算完了之后存下来,每次调用firstName的时候把存的值给它。lazy实际上是一个函数,接收一个函数类型,返回一个Lazy对象,这个对象本身就是一个代理,它代理了firstName:

Kotlin真香系列第五弹:类型进阶_第38张图片

既然是代理了getter,所以它内部有一个getValue()方法,如果你想实现属性代理,比如代理getter的话,那么你就要有一个operator function getValue,就像下面这样:

Kotlin真香系列第五弹:类型进阶_第39张图片

然后参数里面有个thisRef和property,thisRef就是你代理的某个属性的receiver的实例,property就是你代理的某个属性对应的属性引用:

Kotlin真香系列第五弹:类型进阶_第40张图片

②、observable

属性代理除了lazy还有就是observable,给一个属性做代理无非就是setter和getter,如果对一个属性做修改,刚才代理的是getter,现在还可以代理setter,每次设置的时候可以进行拦截:

Kotlin真香系列第五弹:类型进阶_第41张图片  Kotlin真香系列第五弹:类型进阶_第42张图片

定义一个属性state,用by关键字将属性代理定义出来,Delegates.observable(0)实际上是创建了另一个对象,这个对象有setValue和getValue,每次setValue的时候就去执行上面图中的lambda表达式,这样就可以进行拦截实现监听了:

Kotlin真香系列第五弹:类型进阶_第43张图片

函数observable实现了ReadWriteProperty这个接口,它里面创建了个对象ObservableProperty,有一个初始值,这个对象有getValue和setValue的方法。

注意:实现属性代理,如果val类型的变量,实现getValue就可以了,如果是var类型的变量,setValue和getValue都要实现。

Kotlin真香系列第五弹:类型进阶_第44张图片

五、单例类

①、object的定义

Java中写单例有好几种方式,不过在Kotlin中官方语言就已经支持了一种单例的写法——object,下面图中的这种写法它本身就是一种饿汉式的单例,类一旦加载的时候它立即进行初始化:

Kotlin真香系列第五弹:类型进阶_第45张图片

②、成员访问

如何访问object的成员呢?直接访问,比较简单:

Kotlin真香系列第五弹:类型进阶_第46张图片

使用注解@JvmStatic可以在JVM上得到静态成员,注意Kolin作为一门跨平台语言,本身是没有静态成员的概念的:

Kotlin真香系列第五弹:类型进阶_第47张图片

使用注解@JvmField标记在JVM平台可以得到没有setter和getter的成员,等同于Java中的field:

Kotlin真香系列第五弹:类型进阶_第48张图片

③、伴生对象

普通类标记为静态成员是不可以的:

Kotlin真香系列第五弹:类型进阶_第49张图片

但是在类的伴生对象(companion object)中是可以的:

Kotlin真香系列第五弹:类型进阶_第50张图片

也就是说:下面的Kotlin代码等价到Java中就是左侧这样的:

Kotlin真香系列第五弹:类型进阶_第51张图片

而在普通类中使用@JvmField那么就是下面这种情况:

Kotlin真香系列第五弹:类型进阶_第52张图片

object不能自定义构造器,但是可以定义init块:

Kotlin真香系列第五弹:类型进阶_第53张图片  

Kotlin真香系列第五弹:类型进阶_第54张图片

object的类可以继承父类或者实现接口,这一点与普通的类是一致的:

Kotlin真香系列第五弹:类型进阶_第55张图片

六、内部类

①、内部类的定义

内部类在Java中咱们也是学过的,内部类有非静态内部类和静态内部类,它们二者的区别在于:非静态内部类需要引用外部类的实例,而静态内部类无需引用外部类的实例,因此我们经常会遇到非静态内部类导致的内存泄露的问题。相对应的在Kotlin中定义时:非静态内部类使用inner关键字标记,静态内部类不需要inner关键字,这一点跟Java里面是反过来的,需要注意:

Kotlin真香系列第五弹:类型进阶_第56张图片

②、内部类实例化

非静态内部类需要构造出一个外部类的实例,静态内部类直接外部类.调用即可:

Kotlin真香系列第五弹:类型进阶_第57张图片

③、内部object

内部object不存在非静态的情况,故不可用inner修饰,它定义出来的全部都是静态的:

Kotlin真香系列第五弹:类型进阶_第58张图片

④、匿名内部类

匿名内部类如果定义在静态区域(companion object或者顶级函数等)则不会造成内存泄露,如果定义在非静态区域则容易造成内存泄露:

Kotlin真香系列第五弹:类型进阶_第59张图片

既然object省略了名字就是匿名内部类了,那么它是不是可以实现多个接口呢?答案是可以的,Kotlin中的匿名内部类可以继承一个父类同时实现多个接口,注意这一点在Java中是不支持的:

Kotlin真香系列第五弹:类型进阶_第60张图片

但是:如果你想在一个方法中定义一个类来实现多个接口的话,可以使用本地类,并且Kotlin中也是支持的(仅作了解):

Kotlin真香系列第五弹:类型进阶_第61张图片

并且Kotlin还支持本地函数,Java不支持(仅作了解):

Kotlin真香系列第五弹:类型进阶_第62张图片

七、数据类

7.1、data class的定义

普通类前面加上data标记为数据类,

Kotlin真香系列第五弹:类型进阶_第63张图片

虽然从表面上看只是加了一个data关键字,但是实际上编译器帮我们做了很多事情。很多人习惯上把它当做JavaBean来使用,但是实际上这二者是不相等的:

Kotlin真香系列第五弹:类型进阶_第64张图片

因为Kotlin的数据类中定义在主构造器中的属性又称为component,所有的东西都是基于component来实现的,比如下图中通过component1()这样的方法可以获取到对应位置上的属性:

Kotlin真香系列第五弹:类型进阶_第65张图片

还记得之前的文章中有说到过一种Pair的数据结构吗,pair有一种解构的写法用来模拟函数返回多个值,它的原理是什么呢?如下图中的这个例子,hello和world是分别传给了first和second,也就是component1和component2,这样一来,咱们就可以用解构的方式把所有的component都解构出来:

Kotlin真香系列第五弹:类型进阶_第66张图片

同样的道理,咱们的data class也可以做解构,解构时相应的id、name、author对应的就是component1、component2、component3,所以在数据类中一个很关键的概念就是component,这个是普通类没有的:

Kotlin真香系列第五弹:类型进阶_第67张图片

7.2、Java Bean和data class比较

Kotlin真香系列第五弹:类型进阶_第68张图片

Kotlin真香系列第五弹:类型进阶_第69张图片

7.3、如何合理使用data class

①、数据类首先应该是作为数据结构来使用,就是纯数据,大多数情况下不需要额外实现:

Kotlin真香系列第五弹:类型进阶_第70张图片

②、属性类型最好是基本类型或者其它数据类,这样不会有其它逻辑产生,能够保证就是纯数据:

Kotlin真香系列第五弹:类型进阶_第71张图片

③、component不能自定义setter和getter:

Kotlin真香系列第五弹:类型进阶_第72张图片

④、属性最好定义为val类型:

Kotlin真香系列第五弹:类型进阶_第73张图片

7.4、如何作为Java Bean使用

通过上面的对比我们也能够发现想作为JavaBean使用,有两点需要考虑:一是final,二是主构造器,正是这两点限制了data class作为Java Bean来使用,官方给出了一些解决方案:

Kotlin真香系列第五弹:类型进阶_第74张图片

使用NoArg插件,给数据类添加一个注解,这样可以在编译时生成默认的无参构造,使用AllOpen插件去除final标记。

具体使用方式参考官方文档:https://www.kotlincn.net/docs/reference/compiler-plugins.html

我这里说一下Android Studio中如何使用吧:

①、定义一个注解:空注解即可

public @interface Jarchie {
}

②、添加依赖:

工程的build.gradle中:

classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"

app module的build.gradle中:

apply plugin: "kotlin-noarg"
apply plugin: "kotlin-allopen"

noArg{
    invokeInitializers = true
    annotations "com.jarchie.kotlinpractices.advancetypes.Jarchie"
}

allOpen{
    annotations "com.jarchie.kotlinpractices.advancetypes.Jarchie"
}

③、定义数据类,并且使用前面创建的注解标注:

@Jarchie
data class Book(val id:Long,val name:String,val author:String)

OK,这样就搞定了,然后可以反编译看一下是否修改成功 show kotlin bytecode:

Kotlin真香系列第五弹:类型进阶_第75张图片 Kotlin真香系列第五弹:类型进阶_第76张图片

可以看到Book已经不是final类型的了,并且最后还生成了一个无参的构造方法。

八、枚举类

①、枚举类的定义

对于枚举类的定义,在Kotlin中和Java中几乎是一样的,就是Kotlin中多了个class,如下图中所示左侧Java右侧Kotlin:

Kotlin真香系列第五弹:类型进阶_第77张图片

②、枚举的属性

跟Java中的获取方式类似,只不过Kotlin中是属性不是方法了,下图中ordinal和name是内置属性,即:序号和名称:

Kotlin真香系列第五弹:类型进阶_第78张图片

③、定义构造器

Kotlin中跟普通类的定义方式是一样的,定义完了之后每个枚举的实例都需要调用构造器给它传递相对应的参数进去:

Kotlin真香系列第五弹:类型进阶_第79张图片

④、实现接口

跟普通的接口实现是一样的,只不过这里分为统一实现和各自实现:

统一实现:每个枚举实例的run()方法实现都是一样的

Kotlin真香系列第五弹:类型进阶_第80张图片

各自实现:每个枚举实例单独定义run()方法

Kotlin真香系列第五弹:类型进阶_第81张图片

⑤、定义扩展

为枚举定义扩展,这一点是Kotlin所特有的了,Java中是没有的,比如下面图中定义的扩展方法,获取枚举的下一个元素:

Kotlin真香系列第五弹:类型进阶_第82张图片

⑥、条件分支

因为枚举本身的个数是有限的,所以枚举本身是完备的,分支语句中可以写成下面这种格式:

Kotlin真香系列第五弹:类型进阶_第83张图片

⑦、枚举的比较

枚举是有顺序的,所以枚举还可以用来比大小:

Kotlin真香系列第五弹:类型进阶_第84张图片

⑧、区间

既然枚举是有顺序的,所以它自然也就有区间了,这个也很容易理解吧:

Kotlin真香系列第五弹:类型进阶_第85张图片

九、密封类

①、密封类的概念

这个东西在Java里面是没有的,所以在这里咱们先来看一下什么是密封类:

  • 密封类是一种特殊的抽象类
  • 密封类的子类定义在与自身相同的文件中
  • 密封类的子类的个数是有限的

②、密封类的定义

那密封类该如何定义呢?如下图所示,它既然是个抽象类那么也就表明它可以被继承,使用sealed关键字标识:

构造器直接私有,正因为私有所以外部无法调用到它的构造器,正因为无法调到构造器所以外部无法继承:

Kotlin真香系列第五弹:类型进阶_第86张图片

③、密封类的子类

密封类的子类该如何定义呢?跟普通的抽象类的继承其实也差不多:

Kotlin真香系列第五弹:类型进阶_第87张图片

因为它的子类是完备的,所以对一个状态进行判断的时候,可以使用这种类似枚举的形式,它跟枚举不同的是它在保证类型完备的情况下还可以创建新的对象:

Kotlin真香系列第五弹:类型进阶_第88张图片

④、密封类和枚举类的区别

Kotlin真香系列第五弹:类型进阶_第89张图片

写段代码实际感受一下吧:

//region entity
data class Song(val name: String, val url: String, var position: Int)

data class ErrorInfo(val code: Int, val message: String)

object Songs {
    val StarSky = Song("Star Sky", "https://fakeurl.com/321144.mp3", 0)
}
//endregion

//region state
sealed class PlayerState

object Idle : PlayerState()

class Playing(val song: Song) : PlayerState() {
    fun start() {}
    fun stop() {}
}

class Error(val errorInfo: ErrorInfo) : PlayerState() {
    fun recover() {}
}
//endregion

class Player {
    var state: PlayerState = Idle

    fun play(song: Song) {
        this.state = when (val state = this.state) {
            Idle -> {
                Playing(song).also(Playing::start)
            }
            is Playing -> {
                state.stop()
                Playing(song).also(Playing::start)
            }
            is Error -> {
                state.recover()
                Playing(song).also(Playing::start)
            }
        }
    }
}

fun main() {
    val player = Player()
    player.play(Songs.StarSky)
}

十、内联类

①、内联类的概念

  • 内联类是对某一个类型的包装
  • 内联类是类似于Java装箱类型的一种类型
  • 编译器会尽可能使用被包装的类型进行优化
  • 内联类在Kotlin1.3中处于公测阶段,谨慎使用

②、内联类的定义

Kotlin真香系列第五弹:类型进阶_第90张图片

可以给内联类定义方法:

Kotlin真香系列第五弹:类型进阶_第91张图片

③、内联类的属性

要求必须不能有backing field,换句话说就是内联类中只能定义函数了:

Kotlin真香系列第五弹:类型进阶_第92张图片

④、内联类的使用场景

官方的使用场景:内联类实现无符号类型

Kotlin真香系列第五弹:类型进阶_第93张图片

自定义的实现场景:内联类模拟枚举

Kotlin真香系列第五弹:类型进阶_第94张图片

⑤、内联类的继承结构

内联类可以实现接口,但是不能继承父类也不能被继承:

Kotlin真香系列第五弹:类型进阶_第95张图片

这里实现了Comparable接口之后就可以直接和整型比较大小了:

Kotlin真香系列第五弹:类型进阶_第96张图片

⑥、内联类的限制

  • 主构造器必须有且仅有一个只读属性
  • 不能定义有backing-field的其他属性
  • 被包装类型必须不能是泛型类型
  • 不能继承父类也不能被继承
  • 内联类不能定义为其他类的内部类

⑦、别名和内联类的区别

Kotlin真香系列第五弹:类型进阶_第97张图片

今天的内容有点多了,就先写到这里吧,好多概念性的东西,肯定需要反复练习才能加深记忆,只看一遍可能现在会了,不过过一会估计又该忘了,还是得边学边练理解记忆!

好了,不多说了,有点啰嗦了,今天就到这里了,咱们下期再会!

祝:工作顺利!

你可能感兴趣的:(Kotlin)