【问题标题】:How to express in Kotlin "assign value exactly once on the first call"?如何在 Kotlin 中表达“在第一次调用时只赋值一次”?
【发布时间】:2021-07-14 05:12:34
【问题描述】:

寻找一种自然的 Kotlin 方式,让 startTime 仅在特定位置初始化一次。

下面的幼稚实现有两个问题:

  1. 它不是线程安全的
  2. 它没有表示“变量在 Item 实例的生命周期中被或将被分配一次”

class Item {
    var startTime: Instant?

    fun start(){
        if (startTime == null){
            startTime = Instant.now()
        }

        // do stuff
    }
}

我相信某种类型的委托可能适用于此。换句话说,这段代码需要类似于lazy 变量的东西,但在第一次读取时没有初始化,而是仅在显式调用“touching”方法后发生。也许Wrap 调用可以提供可能实现的想法。

class Wrap<T>(
  supp: () -> T
){
   private var value: T? = null
   private val lock = ReentrantLock()
  
   fun get(){
     return value
   }

   fun touch(){
      lock.lock()

      try{
          if (value == null){
              value = supp()
          } else {
              throw IllegalStateExecption("Duplicate init")
          }
      } finally{
        lock.unlock()
      }
   }
}

【问题讨论】:

  • 我不明白为什么lazy() 不适合您的情况。如果您的“接触”方法只是在惰性值上调用 read 来初始化它怎么办?当有人在调用“触摸”方法之前尝试读取值时,您的预期行为是什么?
  • @broot 值有一个初始状态(即null),阅读器在“触摸”之前读取值时会得到它
  • 我完全重写了我的答案以满足仅从 start() 方法设置值的要求。

标签: kotlin design-patterns delegates lazy-evaluation


【解决方案1】:

AtomicReference.compareAndSet 与自定义backing field 组合起来怎么样?

您可以使用私有设置器并确保该类设置值的唯一位置是来自start() 方法。

class Item(val value: Int) {
    private val _startTime = AtomicReference(Instant.EPOCH)
    var startTime: Instant?
        get() = _startTime.get().takeIf { it != Instant.EPOCH }
        private set(value) = check(_startTime.compareAndSet(Instant.EPOCH, value)) { "Duplicate set" }

    fun start() {
        startTime = Instant.now()
    }

    override fun toString() = "$value: $startTime"
}

fun main() = runBlocking {
    val item1 = Item(1)
    val item2 = Item(2)
    println(Instant.now())
    launch { println(item1); item1.start(); println(item1) }
    launch { println(item1) }
    delay(1000)
    println(item2)
    item2.start()
    println(item2)
    println(item2)
    item2.start()
}

示例输出:

2021-07-14T08:20:27.546821Z
1: null
1: 2021-07-14T08:20:27.607365Z
1: 2021-07-14T08:20:27.607365Z
2: null
2: 2021-07-14T08:20:28.584114Z
2: 2021-07-14T08:20:28.584114Z
Exception in thread "main" java.lang.IllegalStateException: Duplicate set

【讨论】:

  • 哇!富有表现力和令人印象深刻的代码。谢谢。
【解决方案2】:

我认为您的Wrap 课程是实现这一点的良好起点。我肯定会将它设为属性委托,touch() 可以大大简化:

fun touch() {
    synchronized(this) {
        check(value == null) { "Duplicate init" }
        value = supp()
    }
}

然后你可以删除lock。但总的来说,这是一个很好的方法。

如果您想重用 stdlib 中的 lazy util,那么您可以通过将其包装在另一个对象中来做到这一点,该对象在被询问之前不会读取其值:

class ManualLazy<T : Any>(private val lazy: Lazy<T>) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
        return if (lazy.isInitialized()) lazy.value else null
    }

    fun touch() {
        lazy.value
    }
}

class Item {
    private val _startTime = ManualLazy(lazy { Instant.now() })
    val startTime: Instant? by _startTime

    fun start(){
        _startTime.touch()
    }
}

当然,根据您的需要,您可以使用类似的技术以非常不同的方式实现它。

这可能被认为是利用或破解惰性工具。我同意,我认为Wrap 方法更好。

【讨论】:

  • 我喜欢你的想法。为了实施解决方案,我将把您的答案与 Adam 的答案结合起来,将 Item 与这些 get/set 例程分离。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多