Kotlin的一些技巧和迂回操作

Author Avatar
Dexlind 1月 08, 2018
  • 在其它设备中阅读本文章

RT,不定期更新。

目录:

不依赖getter和setter的lateinit属性

lateinit属性之所以无法自定义getter和setter,是因为需要在getter插入空检测,并且保证setter能给backing field赋值。

迂回方法如下:

@UseExperimental(ExperimentalContracts::class)
inline fun trick(willNotBeInvoked: () -> Unit) {
   contract { callsInPlace(willNotBeInvoked, InvocationKind.EXACTLY_ONCE) }
}

fun <T> undefined() : T = throw Exception()

class Some {
   var lateInit: String
   init {
      trick { lateInit = undefined() }
   }
}

var lateInit: String
val no_use = trick { lateInit = undefined() }

原理:利用contracts欺骗Kotlin编译器,让它以为属性已被正常初始化(然而并不),属性停留在未被初始化的阶段,从而模拟 lateinit 的功能。当然安全性需要由敲代码的人来保证了。

因为是假的lateinit,所以反射 Some::lateInit.isLateinit 将返回 false。

用途:自定义getter或setter、@JvmField

解除内联类的一些限制

不开后门的话,Kotlin1.3的新功能内联类的作用将十分有限。

// 非公有构造器,以及泛型
@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS")
inline class Some<T> private constructor(val s: T)
// 非顶层声明
class Outer {
   @Suppress("INLINE_CLASS_NOT_TOP_LEVEL")
   inline class Inner(val s: String)
}

可能有Bug(逃

开启残废的SAM转换功能

Kotlin1.3版本有用,没试过更旧的版本。

添加编译器参数:

-XXLanguage:+NewInference
-XXLanguage:+SamConversionForKotlinFunctions

以下代码能通过编译:

fun test(runnable: java.lang.Runnable) {
   test {}
}

然而如下代码仍然不能通过编译:

interface CanRun {
   fun run()
}

fun test(runnable: CanRun) {
   test {} // 这里报错
}

副作用:NewInference可能存在bug,并且当出现编译错误的时候可能得到一堆难懂的报错信息。

如何添加编译器参数

使用 Gradle:

// build.gradle
compileKotlin {
    ...
    kotlinOptions.freeCompilerArgs += ["-foo", "-bar"]
}

PS:使用Gradle的话,请注意IDEA的 Delegate IDE build/run actions to gradle 这个选项是否勾选的区别。

使用 Gradle Kotlin Dsl:

// build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

tasks.withType<KotlinCompile> {
    ...
    kotlinOptions.freeCompilerArgs += listOf("-foo", "-bar")
}

// 或者是下面这样
val compileKotlin: KotlinCompile by tasks
kotlinOptions.freeCompilerArgs += listOf("-foo", "-bar")

IDEA项目,不使用Maven、Gradle构建工具的情况:

有两个地方:一个是对整个Project有效的全局设置;另一个对单独Module有效,可以覆盖全局设置。

全局设置:File -> Settings -> 找到 Kotlin Compiler -> Additional command line parameters

Module设置:File -> Project Structure -> Module -> 找到目标Module里面的Kotlin设置 -> Additional command line parameters

如果Module设置没有Kotlin的话,可以点击「+」按钮手动加上 Kotlin 设置。

不需要import就能使用的顶层函数

一个顶层函数,除非你在同一个文件里使用,否则就需要 import 或者使用完全限定名。问题是有些人就是嫌烦,想要所谓的“全局函数”,就像 Kotlin 标准库里的 println 一样。

其实很简单,只需要写得跟 println 一样就行了:

package kotlin

fun fuck() {}

因为 kotlin 包下的东西都是自动导入的,也就不需要自己动手导入啦。

需要传入编译器参数 -Xallow-kotlin-package 来允许使用 kotlin 开头的包名。

递归的Lambda表达式

在某个 Kotlin 裙里看到有人在问:

是不是lambda无法递归

举个例子,我们可以写一个简单的递归函数:

fun a() { println("1551"); a() }
a() // 打印出很多1551

如果要写成 Lambda 呢?这样的代码会报错:

val a: () -> Unit = { println("1551"); a() }

我们自然是不能直接写这样的代码的,它会说 a 没有定义。解决方法当然是使用 lateinit

lateinit var a: () -> Unit
a = { println("1551"); a() }
a() // 打印出很多1551

更进一步:匿名 Lambda 表达式的递归

正统的「Lambda演算」里面的函数全部都是匿名函数,需要使用「不动点组合子」实现递归:

// 这是kotlin-js
val z = { f: dynamic ->
   { g: dynamic -> g(g) } { x: dynamic -> f { y: dynamic -> x(x)(y) } }
}
val a = z { f: () -> Unit ->
   {
      println("1551"); f()
   }
}
// 求斐波那契数列第n项的函数
val fib: (Int) -> Int = z { f: (Int) -> Int ->
   { x: Int ->
      if (x <= 2) 1
      else f(x - 1) + f(x - 2)
   }
}
// 输出斐波那契数列前10项
println((1.rangeTo(10).map(fib)))

上面的那一坨 val z 即是「Z组合子」。(读者可以思考一下为什么这里我给了 Kotlin-js 的例子是而不是 Kotlin-jvm(逃

阻止编译器添加对非空类型的NullCheck

总所周知,当一个函数的参数是非空类型时,Kotlin编译器会在方法入口处加一行检查入参是否为空的代码。比如说 main 函数:

fun main(args: Array<String>) {}

经过编译后,再反编译成Java:

public static final void main(@NotNull String[] args) {
    Intrinsics.checkParameterIsNotNull(args, "args");
}

可恶!辣鸡编译器自作主张!我不想要这行代码!

如果不想编译器生成这些代码,需要添加编译器参数,如下

-Xno-call-assertions
-Xno-param-assertions
-Xno-receiver-assertions

给主构造器内的属性自定义getter和setter

data class SomeClass(var name: String)

众所周知 Kotlin 不允许给声明在主构造器里面的属性写自定义getter、setter,主要是为了防止有好事者乱写,破坏规则就不好了。所以迂回操作如下:

data class SomeClass(private var _name: String) {
    var name: String
        get() = _name
        set(value) { _name = value }
}

解释:private_name不会生成getter和setter,你再把你想写的getter和setter添上就好。这样SomeClass里面就有3样东西:String _nameString getName()void setName(String)(以及data class根据_name自动生成的那些)。

缺点很明显,toString 生成的字符串会比较丑。

流的读取

普通青年:

// java 代码
void someFunc(InputStream in, OutputStream out) throws IOException {
    int read;
    while ((read = in.read()) != -1) {
        out.write(read);
    }
}

文艺青年:

fun someFunc(`in`: InputStream, out: OutputStream) {
    var read: Int = -1
    while ({ read = `in`.read();read }() != -1) {
        out.write(read)
    }
}

二逼青年:

fun someFunc(`in`: InputStream, out: OutputStream) {
    var read: Int = `in`.read()
    while (read != -1) {
        out.write(read)
        read = `in`.read()
    }
}

天哪,真是太完美了:

fun someFunc(`in`: InputStream, out: OutputStream) {
    var read: Int = -1
    while (`in`.read().also { read = it } != -1) {
        out.write(read)
    }
}

在 Kotlin 1.3 版本正式启用了 contracts 功能后,上面这种写法能应对更多情况。

限制扩展的作用域(防止污染命名空间)

注意:此技巧并不稳定,可能在未来被官方干掉。

// 把扩展丢进一个object里面
object StringExtension {
    @JvmStatic fun String.fuck() = println("fuck $this")
}
// 使用说明
fun test() {
    // 下面这行被注释掉的代码不能通过编译
    // "kotlin".fuck()

    // 你要这么用,将MyExtentions塞进上下文(即this)
    with(StringExtention) {
        "kotlin".fuck()
    }
}
// 或者手动引入
import StringExtension.fuck

fun test() {
    "kotlin".fuck()
}
// 以下是夏姬八写,别模仿
interface Extension

inline fun <T : Extension, R> T.use(block: T.() -> R) = this.block()

object StringExtension : Extension {
    @JvmStatic fun String.fuck() = println("fuck $this")
}

object IntExtension : Extension {
    @JvmStatic fun Int.love() = println("I love $this")
}

fun test() {
    StringExtension.use { "kotlin".fuck() }

    IntExtension.use { 1551.love() }
}

链式调用时输出中间值

inline fun <T> T.println(): T = printlnBy { it }

inline fun <T, U> T.printlnBy(selector: (T) -> U): T = this.also { println(selector(it)) }

fun test() {
    listOf(1, 2, 3).asSequence()
            .map { it * 3 }.printlnBy { it.sum() } // <==这里
            .filter { it and 1 == 0 }
            .sum().println() // <==还有这里
}
// 输出:
// 18
// 6

注意副作用,别夏姬八用!

如果是集合操作,可以考虑使用 onEach 这个高阶函数,例如onEach { println(it) }

知识共享许可协议
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。

本文链接:https://aisia.moe/2018/01/07/kotlin-jiqiao/