kotlin 让`lazy`在值更改时重新计算

y53ybaqx  于 2023-10-23  发布在  Kotlin
关注(0)|答案(3)|浏览(168)

我想在普通JVM中使用类似Android Compose的remember(key) { init }的东西Kotlin。因此,某种类型的Lazy委托,但在每次访问时,它都会调用一个键函数,如果其返回值更改,则会重新计算该值。是否有一个简单的标准库功能来实现这一点,或者我需要创建一个自定义类?

rm5edbpk

rm5edbpk1#

首先,没有简单直接的方法来解决这个问题。问题是,我们无法观察到一个常规属性或函数的值,所以我们不知道什么时候重新计算值。
根据我们的需求,有多种可能的解决方案。一种是提供一种方法来使缓存的值无效,因此创建一个与Lazy非常相似的实用程序,但具有额外的invalidate()功能。
另一个解决方案是你建议的:指定某种密钥,用于确定是否需要重新计算。获取当前键的代码应该非常轻量级,因为每次访问属性时都会调用它。示例实施方式:

fun <K, V> recalculatingLazy(keyProvider: () -> K, valueProvider: (K) -> V) = object : ReadOnlyDelegate<V> {
    private var lastKey: Any? = NotSet
    @Suppress("UNCHECKED_CAST")
    private var lastValue = null as V

    override fun getValue(thisRef: Any?, property: KProperty<*>): V {
        val key = keyProvider()
        return if (key == lastKey) {
            lastValue
        } else {
            lastKey = key
            valueProvider(key).also { lastValue = it }
        }
    }
}

private object NotSet

interface ReadOnlyDelegate<T> {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T
}

使用方法:

fun main() {
    var source = 1
    val target by recalculatingLazy({ source }) {
        println("New key: $it, recalculating...")
        it * 2
    }

    println(target) // recalculating...
    println(target)
    source = 2
    println(target) // recalculating...
    println(target)
}

请注意,此实现不是线程安全的。
在实践中,这种解决方案是非常有限的,我们只能在非常特定的情况下使用它,因为我们需要一个关键字来识别更改。这类问题通常通过另一种解决方案来解决-通过创建可观察变量。我们有一个非常特定类型的源变量A,可以观察它的变化。然后目标变量B观察它,因此它知道何时重新计算。
多年来,我们大大改进了这一概念:我们从一个简单的Observer Pattern开始,但是它有很多缺点。然后是Reactive Streams。Kotlin通过利用协程改进了这个概念-它提供了flows

suspend fun main(): Unit = coroutineScope {
    val source = MutableStateFlow(1)
    val target = source.map {
        println("New key: $it, recalculating...")
        it * 2
    }

    launch {
        target.collect { println("Value: $it") }
    }

    delay(500)
    source.value = 2
    delay(500)
    source.value = 3
    delay(500)
}

请注意,这是完全不同的概念,一切都是异步工作的。我们可以重写这个例子,直接询问target变量,就像前面的例子一样,但是我们不能保证在设置了source之后,target什么时候会被更新。这是故意的行为。

nfeuvbwi

nfeuvbwi2#

不,没有内置的方法来做到这一点。您必须做一些自定义的事情,尽管您可能会发现研究Lazy如何工作以自己复制它是有用的。

gmxoilav

gmxoilav3#

lazy不能做到这一点,但任何简单的缓存应该能够做到。只需将其最大大小设置为1,从该高速缓存中获得的任何新密钥都将替换以前的值。
下面是一个基于Caffeine缓存库的例子:

import com.github.benmanes.caffeine.cache.Caffeine

class CachedValue<K, V>(loader: (K) -> V) {
    private val cache = Caffeine.newBuilder().maximumSize(1).build(loader)
    
    fun get(key: K): V = cache.get(key)
}

相关问题