Kotlin的一些技巧和迂回操作
RT,不定期更新。
目录:
- 扩展属性的backing field
- 不依赖getter和setter的lateinit属性
- 解除内联类的一些限制
- 开启残废的SAM转换功能
- 如何添加编译器参数
- 不需要import就能使用的顶层函数
- 递归的Lambda表达式
- 阻止编译器添加对非空类型的NullCheck
- 给主构造器内的属性自定义getter和setter
- 流的读取
- 限制扩展的作用域
- 链式调用时输出中间值
扩展属性的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
模式。比如说安卓项目,例如 compileDebugKotlin
、 compileReleaseUnitTestKotlin
等。
使用 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 _name
,String 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/