Kotlin:在自定义构建器函数中初始化 vals

问题描述 投票:0回答:1

我创建了一些自定义数据类型和相应的构建器,它们在单元测试中大量使用。一般模式看起来有点像这样:

@Test
fun testSomething(){
  object {
    val someVal: SomeType
    val someOtherVal: SomeOtherType
    init {
      // everything is a `val`, properly declared and type safe
      with(Builder()){
        someVal = type(/*args*/).apply {
          someOtherVal = otherType(/*other args*/)
        }
      }
    }
  }.run {
    // Run Code and do Assertions...
    // notice how I can use `someVal` and `someOtherVal` directly (woohoo scopes)
  }
}

对应的类型和构建器:

class SomeType()
class SomeOtherType(parent: SomeType)
class Builder(){
  fun type() = SomeType()
  fun SomeType.otherType() = SomeOtherType(parent = this)
}

然后我意识到我太懒了,总是在父类型上调用

apply
,而是想将对
apply
的调用移到构建器中。研究它的定义我确实意识到我需要
inline
关键字:

// within the Builder: redefine function `type`
inline fun type(block: SomeType.()->Unit = {}) = SomeType().apply(block)

在单元测试中:

// within the init:
with(Builder()){
  someVal = type(/*args*/) {
    // I saved the `apply` call and thus  6 characters, time to get some ice cream
    someOtherVal = otherType(/*other args*/)
  }
}

但现在 IntelliJ 标记

someOtherVal
并给出错误

[CAPTURED_MEMBER_VAL_INITIALIZATION] 由于可能重新分配,禁止捕获的成员值初始化

这是可以理解的,但这也意味着第一个示例中的

apply
函数是不可能的。有人可能会说编译器知道发生了什么,因为
apply
是标准库的一部分。但它只是一个库,而不是一个语言功能。

我可以消除错误并修复我自己的构建器,还是必须坚持手动调用

apply

顺便说一句:将变量定义为

lateinit
var
不是解决方案。

kotlin scope initialization builder val
1个回答
0
投票

您可以分配给

val
块中的
with
,因为
with
保证会立即调用您给它的 lambda 一次

编译器不知道您的

type
方法也立即调用 lambda 一次。就编译器而言,lambda 很可能被多次调用,逃逸到其他地方,或者其他什么。如果是这种情况,那就意味着
someOtherVal
被分配了多次,而这种情况不会发生,因为它是
val

您可以执行

with
的操作,并使用实验性合约 API:

@OptIn(ExperimentalContracts::class)
inline fun type(block: SomeType.()->Unit = {}): SomeType {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return SomeType().apply(block)
}

由于这似乎是一个仅用于测试的临时对象,因此我宁愿将

someOtherVal
改为
lateinit var
。或者完全改变
Builder
的设计。
Builder
应该只返回 one 事物,并且您可以将该构建事物的属性分配给您的
val

© www.soinside.com 2019 - 2024. All rights reserved.