Kotlin的一些技巧和迂回操作

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

RT,不定期更新。

目录:

扩展属性的backing field

众所周知扩展属性是没有backing field的,它其实就是扩展函数的特殊形式。但是总有人想要给某个类扩展一个真正的field,就像下面那样。

class Some {}

// unresolved reference: field
var Some.rua : String
   get() = field
   set(value) {
      field = value
   }

这种需求针对不同的实际情况有不同的解决方法,下面给出一个非最优但是比较通用的方法。

val ruaMap: MutableMap<Some, String> = TODO() // 这个TODO别直接抄了

var Some.rua : String
   get() = ruaMap[this] ?: ""
   set(value) { ruaMap[this] = value }

我们总要找个地方将数据存起来,这里选择了一个全局的Map来解决存储问题。

需要注意的是,这里对Map的类型有要求:因为不能干扰jvm的垃圾回收机制,所以需要是WeakReferenceMap;因为每个实例都需要保存一份自己的数据,所以需要是IdentityMap;如果在多线程环境跑这代码,需要是ConcurrentMap。综上你需要一个ConcurrentWeakIdentityMap来解决这个问题。

不依赖getter和setter的lateinit属性

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

迂回方法如下:

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.contracts.InvocationKind

@OptIn(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() }

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

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

用途:当你需要给lateinit属性自定义getter或setter时、或者需要一个lateinit的@JvmField。

2020/7/13更新:

更新一个不使用contract的方法,比较简单:

class Some {
   @Suppress("MUST_BE_INITIALIZED_OR_BE_ABSTRACT")
   var lateInit: String
}

@Suppress("MUST_BE_INITIALIZED")
var lateInit: String

利用我那篇文章介绍的方法,用Suppress注解消除Kotlin编译器的编译错误,让属性保持在未初始化的状态。

以上两种方法都需要注意如果直接读取未初始化的属性,会得到null,所以在使用时要格外小心,确保属性的初始化。

解除内联类的一些限制

不开后门的话,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转换功能

Kotlin 1.4 版本是默认开启的,所以不用管。

Kotlin 1.3 版本有用,没试过更旧的版本。

添加编译器参数:

-XXLanguage:+NewInference
-XXLanguage:+SamConversionForKotlinFunctions

以下代码能通过编译:

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

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

interface CanRun {
   fun run()
}

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

如何添加编译器参数

使用 Gradle

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

compileTestKotlin {
    // 单元测试代码的kotlin编译任务,同上
}

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

PPS:对于自定义源文件集(source set)这些任务称呼取决于 compile<Name>Kotlin 模式。比如说安卓项目,例如 compileDebugKotlincompileReleaseUnitTestKotlin 等。

使用 Gradle Kotlin Dsl:

因为我本身不使用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 包下的东西都是自动导入的,所以只需要将你的扩展函数也丢到 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 功能后,上面这种写法能应对更多情况。

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

注意:此方法被官方定义为bug并在1.3.70版本被修复。

object StringExtension {
   fun String.fuck() = println("fuck $this")
}

当你把扩展丢进一个object里面,那么在IDEA里的自动补全列表就不会出现那个扩展。你需要把这个扩展方法完整地敲出来,IDEA才会提示你引入它。

或者你可以用一些方法将object塞进接收者里:

fun test() {
   with(StringExtension) {
      "kotlin".fuck() // 这里会出现自动补全
   }
}

当然把扩展塞进object里的缺点就很明显了,没有自动补全可能一辈子都想不起来有这个扩展方法。

// 以下是夏姬八写,别模仿,只是为了展示某些可能性
interface Extension

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

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

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

@OptIn(ExperimentalContracts::class)
fun importIntExtensionTo(any: Any?) {
   contract { returns() implies (any is IntExtension) }
}

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

   with(Any()) {
      importIntExtensionTo(this)
      42.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/