Kotlin cacheable - 缓存一切函数

背景

在 Kotlin 中,Lazy 是我们经常用到的操作,当我们需要用到时才创建对象,只需要一个 lazy 即可搞定,如下:

kotlin 复制代码
val myObj by lazy { createObj() }

但稍微了解一些 Lazy 背后原理的同学都知道,Lazy 是不完美的,下面列举一下它带来的影响:

  1. 每个类会在 init 的时候创建所有的 Lazy 对象,性能其实不够极致

  2. Lambda 会被翻译成匿名内部类,对包大小也不友好

  3. Lazy 是无输入的,创建完成后,内部的对象永远不再会更改。

    开发过 Intellij 相关内容(Kotlin 编译器 / IDEA 插件)的同学应该知道,Intellij 提供了 Compute 的函数来实现追踪修改的 cache 功能

Cacheable 框架

实现 Lazy

基于 Lazy 的这些问题,我能想到的最简单办法就是给每个 Lazy 对象创建一个 backend field 持有其数据,重写其 Kotlin getter 来实现缓存,伪代码如下:

kotlin 复制代码
// 转换前
@get:Cacheable
val myObj get() = createObj()

// 转换后
var backend_myObj = null
@get:Cacheable
val myObj get() = backend_myObj ?: createObj().also { backend_myObj = it }

我们可以看到,转换后的代码添加了一个幕后属性,并且在幕后属性为空的时候,执行创建操作并赋值幕后属性。当然在实际使用中,我会读取 Cacheable 注解中传递的参数来选择是否生成线程安全的 synchronized 初始化体。我选择了修改 Kotlin IR 来实现这个功能,只需要实现一个 KCP 即可,具体可以看仓库中代码。

追踪变动

我们也希望在函数参数变化的时候,动态决定要不要计算新的 value,下面是一个例子,在每次函数参数与上次不同时(通过 equals 方法),会重新计算函数返回值:

kotlin 复制代码
var a = 0

@Cacheable(cacheMode = CacheMode.TRACK_ARGS)
fun bar(param0: String, param1: Int): Int = ++a

// test function to call cacheable
// only changed when argument changed (through `equals`)
fun test() {
    bar("a", 0) // 1
    bar("a", 0) // 1
    bar("b", 1) // 2
    bar("b", 1) // 2
    bar("c", 2) // 3
    bar("c", 2) // 3
}

这个 bar 背后会生成如下代码(伪代码,实际更复杂):

kotlin 复制代码
var backend_bar = null
var backend_bar$0 = null // 第 0 个参数
var backend_bar$1 = null // 第 1 个参数

@Cacheable(cacheMode = CacheMode.TRACK_ARGS)
fun bar(param0: String, param1: Int) {
    if (param0 == backend_bar$0 && param1 == backend_bar$1) {
        return backend_bar
    } else {
        backend_bar$0 = param0 // 覆盖旧 param0
        backend_bar$1 = param1 // 覆盖旧 param1
        backend_bar = ++a // 计算新值
        return backend_bar
    }
}

当然框架背后的默认逻辑是线程安全的,会自动生成 synchronized + 双重判断代码来保证同样的输入只会有一个输出。

Skiplang 与纯函数

在之前,我写过一篇纯函数的思考文档,其中提到了 skiplang:一个 Kotlin 开发,对于纯函数的思考

Skiplang 的宗旨就在其网站主页,A programming language to skip the things you have already computed,在纯函数的情况下,意味着得知输入状态,那么输出状态就是唯一确定的,这种情况就非常适合做缓存,如果输入值已经计算过,那么直接可以返回缓存的输出值。

事实上本文所介绍的 Cacheable 框架也是类似的思想,它起于我对 Lazy 的不满,最后的结果是进一步朝着纯函数演进。

最后

相关推荐
糖猫猫cc14 小时前
Kite:填充处理器
kotlin·orm·kite
Kapaseker21 小时前
一杯美式深入理解 data class
android·kotlin
alexhilton3 天前
端侧RAG实战指南
android·kotlin·android jetpack
Kapaseker4 天前
2026年,我们还该不该学编程?
android·kotlin
Kapaseker5 天前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
Kapaseker6 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
FunnySaltyFish7 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Kapaseker7 天前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker8 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z10 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin