来看看 Kotlin 1.3-M2 都有哪些好玩的东西


#1

上个月 Kotlin 发布了 1.3-M2,最近 Native 也频繁发版,明眼人一看就知道 1.3 和 Native 1.0 将一起飞上天和太阳肩并肩,嗯看来 Kotlin 又进入了年底冲 kpi 的节奏了。

1.3 各方面的完整更新内容建议阅读官方博客和更新日志,这篇文章则重点对 Contract Dsl、协程、内联类做一些相对深入的介绍。

安装 1.3-M2

安装这个其实比较简单了,比起 Java 什么的,Kotlin 的安装体验简直不好太好(当然你家网不行的话。。。JetBrains 的人大概不会想到世界上还有人网不好吧)。

  1. 请你下载一个新一点儿的 IntelliJ 的版本,比如我现在用的是 2018.2.2
  2. 在 Preference(或者 Settings) ->Languages & Frameworks -> Kotlin Updates 当中,Update channel 选择 “Early Access Preview 1.3”,然后点击 Check again,这时候会提示你有新版:“1.3-M2-release-IJ2018.2-1”,安装,重启 IJ。
  3. 创建新工程,如果你家网不好,那么建议你直接选 Kotlin 工程体验,如果没有 Gradle 下载慢的问题,那么请选择 gradle 工程。

创建工程的时候,注意一下依赖的配置:

plugins {
    id 'java'
    id 'org.jetbrains.kotlin.jvm' version '1.3-M2'
}

注意这个东西大约相当于当年我们的 apply plugin:'kotlin'。这个是现在比较推荐的应用插件的写法,能够这样写的插件都是发布到 Gradle 的官方仓库的,目前 Kotlin 包括 Kotlin-Native 的插件都已经发布到 Gradle 的官方仓库,因此以后主需要这么写就好了。
除了应用插件,你还得依赖 Kotlin 1.3 的标准库:

repositories {
    maven {     url 'http://dl.bintray.com/kotlin/kotlin-eap' }
    ...
}
dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3-M2"
    ...
}

好了,这样你就可以愉快的体验 1.3 的新功能了。

Contract DSL

这个东西其实早就被雀神看穿了,可以参见之前他的一篇文章:Kotlin Contracts DSL

我们简单分析下官方博客给出的两个例子:

fun test(x: Any?) {    
    require(x is String) 
    println("'$x' length is ${x.length}") // 1.3 以前报错
}

这段代码在 1.3 以前是无法通过编译的,原因在于第二句中的 x.length 没有对 x 做判空处理。其实这代码换种写法就通过了:

fun test(x: Any?) {
    if(x is String)
        println("'$x' length is ${x.length}")
}

这么看上去好像也没什么,毕竟字数上没有差多少。那么我们再来看个例子:

fun test2(x: String?) {
    if(!x.isNullOrBlank())
        println("'$x' length is ${x.length}") // 1.3 以前报错
}

我们很容易就能知道在第二行处,x 一定不会为 null,但编译器哪里会去看 isNullOrBlank 到底干了啥,因此编译器不知道,所以你前面的 if 并不能给你带来智能类型转换。

而这段代码从 1.3 开始,就可以运行了。为什么?

public inline fun CharSequence?.isNullOrBlank(): Boolean {
    contract {
        returns(false) implies (this@isNullOrBlank != null)
    }
    return this == null || this.isBlank()
}

contract 就像代码与编译器的约定,编译器不会管你的代码究竟是什么,但它会管 contract 里面的处理结果。这个例子当中,isNullOrBlank 如果返回 false,那么编译器就知道这个 CharSequence 肯定不为空。

还有一个 callsInPlace 的例子:

val x: Int
synchronized(lock) {
    x = 42 // The compiler knows that the lambda is invoked exactly once!
}
println(x) // Allowed now, the compiler knows that 'x' is definitely assigned.

这在 1.3 以前,会报一个错误:Captured values initialization is forbidden due to possible reassignment

因为编译器并不知道你定义的这个 lambda 在什么时候运行,或者运行还是不运行,因此不允许你对外部的只读变量 (val) 进行赋值。

public actual inline fun <R> synchronized(lock: Any, block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    ...
}

而从 1.3 开始,synchronized 跟编译器约定,说我这个 block 一定会运行且只运行一次,而且就原地调用。这样的话对于前面的例子,编译器就知道 x 不会被重复赋值了。

当然,这里有一个小细节需要注意,尽管 callsInPlace 让我们有了 Lambda 原地调用的感觉,但在这里,类型智能转换是对外部无效的,仔细体会一下作用域范围就很容易理解了:

val b: String? = "HelloWorld"
synchronized(lock) {
    require(b != null)
    println(b.length) // 1.3 OK
}
println(b.length) // 1.3 也报错

尽管我们在 synchronized 当中对 b 做了判空,明确知道了 b 一定不会为 null,但这个智能类型转换仅仅在当前作用域内(也就是 { } 内啦)有效,因此后面出了 Lambda 表达式的部分就享受不到优越性了。

val b: String? = "HelloWorld"
synchronized(lock) {
    require(b != null)
    val b1 = b!!  // 如果把代码这样写也许更容易理解一些
    println(b1.length)
}
println(b.length) // 1.3 也报错

终于转正的 “Coroutines”

去年年底的时候,我们本以为协程要转正了。然而 1.2 发版的时候,官方觉得协程还需要再观察一下,于是这件事就被推迟到了 1.3。好在这次不会跳票了,协程相关的包名终于去掉了 “experimental” 的字样,这不仅包括标准库里面的 kotlin.coroutines.experimental 改为了 kotlin.coroutines,kotlinx.coroutines 这个良心框架的包名也随之更新

// kotlin ≤1.2
import kotlinx.coroutines.experimental.launch

//kotlin 1.3
import kotlinx.coroutines.launch

如果你要体验1.3的协程, kotlinx.coroutines 要使用 eap13 对应的版本,例如 ‘org.jetbrains.kotlinx:kotlinx-coroutines-core:0.25.2-eap13’

除了包名的变化外,还有一些比较有用的变化,主要体现在对 suspend 函数的支持上。

作为一个比较特殊的存在,suspend 函数是比较可怜的,例如我们定义下的函数:

suspend fun sendRequest(request: Request): Response{
   ...
}

正常在协程当中调用时没问题的,那么我如果想要写一个那什么一点儿的写法,那么:

launch {
    val requests = listOf<Request>(...)
    requests.mapSuspend(::sendRequest).forEach {
        ...
    }
}

大家注意,我在这里用到了函数引用,然而在 1.3 以前,suspend 函数是不支持的函数引用的。重要的事情说三遍,想必 IntelliJ 也是明白这个道理的吧(/≧▽≦)/:

这个从 1.3 就是可以的了,你会发现协程的代码会越来越自然流畅。

mapSuspend 这个扩展是我自己写的,实现见文末的附录

还有就是官方博客提到的几个反射的方法,此前,我们如果想要通过反射来访问包含协程的代码,那么结果只有。。。。凉凉,因为我们一方面不好判断一个函数是不是 suspend,另一方面也没办法完美的调用这个函数。

suspend 函数编译之后的函数签名其实与我们看到的还不是很一样,如果我们用 Java 代码调用 Kotlin 的 suspend 函数,你就会发现它返回的是一个 Object 类型,而这个 Object 有可能就是你想要的,也有可能是一个叫 COROUTINE_SUSPENDED 的家伙。

现在有了这三个方法,我们一方面可以判断它是不是 suspend 函数,另一方面也可以

  • KCallable.isSuspend,判断是否 suspend
  • KCallable.callSuspend,类似于普通函数的 call,直接传参调用
  • KCallable.callSuspendBy,类似于普通函数的 callBy,参数通过 Map 传入

callSuspendcallSuspendBy 这两个函数调用时,如果调用者 KCallable 不是 suspend 函数,那么这两个函数会转换为 callcallBy 来调用。

可以想到的是,从 1.3 开始,Kotlin 的代码风格要起飞了,如果还是只能按照 Java 的思路写 Kotlin,那么。。。 щ(゚Д゚щ)

内联类 Inline classes

1. 由无符号类型认识内联类

Kotlin 是有梦想的!一个只想着增强 Java 的 Kotlin 跟咸鱼有什么区别!所以 Kotlin 大佬们开始做 Kotlin Native。。。哈哈哈ヽ(;´Д`)ノ

话说,要跟 C 兼容,那么无符号类型得解决一下啊,不然的话 4294967295 可就只能是 Long 了啊。但大佬们一想,UIntInt 除了理解上的差异外,在二进制上有个毛线区别呢?不如我们支持一下内联类 来解决一下这个问题 ⊙﹏⊙|||。

好吧,这些都是我编的。

不过 UInt 确实是通过内联 Int 来实现的:

public inline class UInt internal constructor(private val data: Int) : Comparable<UInt> {
    ...
}

这是什么意思呢?就是在运行的时候根本没有什么所谓的 UInt,不信你试试:

println(UInt.MAX_VALUE::class) // class kotlin.Int

如果我们打印这个最大值,也是可以打出来的

println(UInt.MAX_VALUE) // 4294967295

Kotlin 是怎么做的呢?

// UInt
public override fun toString(): String = toLong().toString()

先转成 Long 再转成 String,这波操作真是 666。那么问题来了,Long 类型怎么转 String 呢?

// ULong
public override fun toString(): String = ulongToString(data)

这个函数的实现大家可以自己去看,反正你需要明白的就是,内联类不过是“障眼法”而已。

2. 一个例子看懂内联类存在的意义

既然说到内联类不过是障眼法,那么我们为什么需要这个障眼法呢?

举个例子,过去一定有人跟你说过尽量不要使用枚举,用 Int 来代替这样的需求场景吧,他的理由一定是枚举会额外生成很多代码,不如 Int 性能好,当然,谁又不清楚枚举有可读性强可维护性好的优点呢?只是有些情况下为了性能而不得不做出妥协罢了。

枚举的写法,创建地图的时候我们只能传入枚举定义好的参数:

enum class MapOptions{
    NORMAL, SATELLITE
}

fun createMap(options: MapOptions): MyMap{
    ...
}

整型的写法,创建地图的时候调用者可能还得翻一下文档才知道这个地方要怎么传参,而且还可以随便传个无意义的整数让逻辑显得很脆弱:

const val SATELLITE = 1
const val NORMAL = 0

fun createMap(options: Int): MyMap{
    ...
}

从 Kotlin 1.1 开始,我们有了 typealias,可以一定程度上缓解整型的问题,虽然没有办法从编译器那里得到帮助,但至少可以有明显的约束让调用者小心谨慎:

typealias MapOptions = Int
const val SATELLITE: MapOptions = 1
const val NORMAL: MapOptions = 0

fun createMap(options: MapOptions): MyMap{
    ...
}

不过,有了内联类,这事儿就可以两全其美了:

inline class MapOptions(private val optionValue: Int)
@JvmField val SATELLITE = MapOptions(1)
@JvmField val NORMAL = MapOptions(0)

fun createMap(options: MapOptions): MyMap{
    ...
}

这样,一方面编译器可以约束调用者必须传入 MapOptions 类型的对象,另一方面编译过后 MapOptions 以及后面定义的两个实例都将转为整型,真是左右逢源。

反编译后的 Java 代码仅供参考:

@JvmField public static final int SATELLITE = 1;
@JvmField public static final int NORMAL = 0;
@NotNull
public static final MyMap createMap_6lku264j(int options) {
    ... //注意:这就是我们的 createMap 函数
}

其他的

按照之前透露的消息,至此,1.3 开放出来的新特性似乎主要就差 Kotlin 的 SAM 了。这个呼声还是比较高的,使用场景也比较容易理解,咱们且等下一个版本再看。

1.3 M2 给我们呈现的,还有一些别的小范围提升,例如增加了一些扩展,给密封类加了个反射的 Api,这些都很容易理解,我就不说了。

小结

Kotlin 过去的几个版本都是很有趣的,

  • 1.0 告诉我们可以放心的使用 Kotlin 了,那时候主打 100% 兼容 Java ,用 Kotlin 来完全替代 Java 已经是非常足够了
  • 1.1 的发版让我们见识了协程,让很多写了多年 Java 的人开始认识到异步编程还会有更优雅的写法
  • 1.2 的发版则推出了 Multiplatform,伴随着 Kotlin-js 的转正,以及 Kotlin Native 的预览,我们大约看到了 Kotlin 的野心
  • 1.3 除了前面提到的一些新特性之外,我们还注意到这段时间 Kotlin Native 已经可以开发 iOS 了,Clion 可以支持 Gradle 开发 Kotlin Native,这大约也是这野心开始的重要时刻吧

我相信 1.3 将是一个非常让人激动的版本,要问它还有哪些好玩的东西,让我们唱一首英特尔的主题曲吧;要问它什么时候来,我想大约会是在冬季。

附录

文中提到的 mapSuspend 方法的实现,很简单的,把标准库里面的 map 稍微改改就好了,话说标准库是不是应该加一个这个扩展,看上去还有点儿用呢:

fun <T> Iterable<T>.collectionSizeOrDefault(default: Int): Int = if (this is Collection<*>) this.size else default

suspend inline fun <T, R> Iterable<T>.mapSuspend(transform: suspend (T) -> R): List<R> {
    return mapSuspendTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

suspend inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapSuspendTo(destination: C, transform: suspend (T) -> R): C {
    for (item in this)
        destination.add(transform(item))
    return destination
}

转载请注明出处:微信公众号 Kotlin


广告时间

如果你有一定的 Android 基础,恰好还想要深入学习Kotlin,建议留意下我的 Kotlin 新课 “基于 GitHub App 业务深度讲解 Kotlin1.2高级特性与框架设计”。

上线之后,大家普遍反映有难度,有深度,如果哪位朋友想要吊打 Kotlin,不妨来看看哦!

https://coding.imooc.com/class/232.html


#2

kotlin 1.3rc

最近更新速度有点快啊


#3

是的,翻译的博客已经发出 https://www.kotliner.cn/2018/09/kotlin-1-3-rc-is-here-migrate-your-coroutines/


#4

现在看来,内联类的限制太多了。比如说在1.2.50里合法的泛型内联类,在1.3rc直接被ban掉了。还有私有主构造器也是报非法。但是标准库里的代码却可以无视这些限制,到处开后门。鸡肋功能+1 :-1:


#5

别急嘛,步子不能迈太大


#6
@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS")
inline class Some<T> private constructor(val s: T)

舒服了


#7

萌雀雀,又舒服了


京ICP备16022265号-2 Kotlin China 2017 - 2018
本站由腾讯云提供计算服务