我创建了一些自定义数据类型和相应的构建器,它们在单元测试中大量使用。一般模式看起来有点像这样:
@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
不是解决方案。
您可以分配给
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
。