I created some custom data types and corresponding builders which are heavily used within unit tests. The general pattern looks somewhat like this:
@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)
}
}
the corresponding types and builders:
class SomeType()
class SomeOtherType(parent: SomeType)
class Builder(){
fun type() = SomeType()
fun SomeType.otherType() = SomeOtherType(parent = this)
}
I then realized that i'm to lazy to always call apply on the parent type and instead wanted to move the call to apply into the builder. Looking into its definition I did realize I will need the inline keyword:
// within the Builder: redefine function `type`
inline fun type(block: SomeType.()->Unit = {}) = SomeType().apply(block)
and within the unit test:
// 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*/)
}
}
But now IntelliJ marks someOtherVal and gives as error
[CAPTURED_MEMBER_VAL_INITIALIZATION] Captured member values initialization is forbidden due to possible reassignment
Which is understandable but this would also mean that it would be impossible for the apply function in the first example. One could argue that the compiler knows whats happening as apply is part of the standard library. But it is just a library, not a language feature.
Can I get rid of the error and fix my own builder or do I have to stick with manually calling apply?
Btw.: Defining the variables as lateinit or var is not a solution.
You are allowed to assign to
vals in awithblock becausewithis guaranteed to call the lambda you gave it exactly once, and immediately.The compiler doesn't know that your
typemethod also calls the lambda exactly once, and immediately. As far as the compiler is concerned, the lambda could very possibly be called more than once, escape to somewhere else, or whatever. And if that is the case, that would meansomeOtherValis assigned multiple times, and this cannot happen because it is aval.You can do what
withdoes, and use the experimental contracts API:Since this seems to be a temporary object just for testing, I'd rather just make
someOtherValalateinit varinstead. Or change the design of theBuildercompletely. TheBuildershould return just one thing, and you can assign the properties of that built thing to yourvals instead.