据说在2019年的JetBrains开放日,Kotlin团队研究了契约(Contract)并试图实现上下文契约(Contract),该契约(Contract)仅允许在某些上下文中调用函数,例如,仅当build
方法恰好被调用一次时才允许调用setName
函数在它之前。 Here是谈话录音。
我尝试使用当前可用的Kotlin功能来模拟此类契约(Contract),以便为data class Person(val name: String, val age: Int)
创建空值安全的生成器。
注意:当然,在这种情况下,使用命名参数而不是构建器模式要容易得多,但是命名参数不允许将未完全构建的对象解析为其他函数,并且在创建时很难使用它们由其他复杂对象等组成的复杂对象。
所以这是我的null安全构建器实现:
基于通用标记的构建器
sealed class Flag {
object ON : Flag()
object OFF : Flag()
}
class PersonBuilder<NAME : Flag, AGE : Flag> private constructor() {
var _name: String? = null
var _age: Int? = null
companion object {
operator fun invoke() = PersonBuilder<OFF, OFF>()
}
}
val PersonBuilder<ON, *>.name get() = _name!!
val PersonBuilder<*, ON>.age get() = _age!!
fun <AGE : Flag> PersonBuilder<OFF, AGE>.name(name: String): PersonBuilder<ON, AGE> {
_name = name
@Suppress("UNCHECKED_CAST")
return this as PersonBuilder<ON, AGE>
}
fun <NAME : Flag> PersonBuilder<NAME, OFF>.age(age: Int): PersonBuilder<NAME, ON> {
_age = age
@Suppress("UNCHECKED_CAST")
return this as PersonBuilder<NAME, ON>
}
fun PersonBuilder<ON, ON>.build() = Person(name, age)
优点:
name
和age
才能构建人。 缺点:
age
,它也必须声明它接受具有任何AGE
类型参数的构建器,并返回具有相同类型参数的构建器。)_name
和_age
属性不能为私有(private)属性,因为应该可以从扩展功能访问它们。 这是此构建器的使用示例:
PersonBuilder().name("Bob").age(21).build()
PersonBuilder().age(21).name("Bob").build()
PersonBuilder().name("Bob").name("Ann") // doesn't compile
PersonBuilder().age(21).age(21) // doesn't compile
PersonBuilder().name("Bob").build() // doesn't compile
PersonBuilder().age(21).build() // doesn't compile
val newbornBuilder = PersonBuilder().newborn() // builder with age but without name
newbornBuilder.build() // doesn't compile
newbornBuilder.age(21) // doesn't compile
val age = newbornBuilder.age
val name = newbornBuilder.name // doesn't compile
val bob = newbornBuilder.name("Bob").build()
val person2019 = newbornBuilder.nameByAge().build()
PersonBuilder().nameByAge().age(21).build() // doesn't compile
fun PersonBuilder<OFF, ON>.nameByAge() = name("Person #${Year.now().value - age}")
fun <NAME : Flag> PersonBuilder<NAME, OFF>.newborn() = age(0)
基于契约(Contract)的构建器
sealed class PersonBuilder {
var _name: String? = null
var _age: Int? = null
interface Named
interface Aged
private class Impl : PersonBuilder(), Named, Aged
companion object {
operator fun invoke(): PersonBuilder = Impl()
}
}
val <S> S.name where S : PersonBuilder, S : Named get() = _name!!
val <S> S.age where S : PersonBuilder, S : Aged get() = _age!!
fun PersonBuilder.name(name: String) {
contract {
returns() implies (this@name is Named)
}
_name = name
}
fun PersonBuilder.age(age: Int) {
contract {
returns() implies (this@age is Aged)
}
_age = age
}
fun <S> S.build(): Person
where S : Named,
S : Aged,
S : PersonBuilder =
Person(name, age)
fun <R> newPerson(init: PersonBuilder.() -> R): Person
where R : Named,
R : Aged,
R : PersonBuilder =
PersonBuilder().run(init).build()
fun <R> itPerson(init: (PersonBuilder) -> R): Person
where R : Named,
R : Aged,
R : PersonBuilder =
newPerson(init)
优点:
Aged
函数中没有提及name
。)缺点:
带有接收器的
this
引用的类型。 where
子句中的样板代码。 PersonBuilder & Named
不是有效的Kotlin语法)。 _name
和_age
属性不能为私有(private)属性,因为应该可以从扩展功能访问它们。 这是此构建器的使用示例:
newPerson {
age(21)
name("Bob")
this // doesn't compile (this type isn't inferred)
}
itPerson {
it.age(21)
it.name("Ann")
it
}
itPerson {
it.age(21)
it // doesn't compile
}
val builder = PersonBuilder()
builder.name("Bob")
builder.build() // doesn't compile
builder.age(21)
builder.build()
是否有更好的null安全生成器实现,并且有什么方法摆脱我的实现弊端?
最佳答案
我认为契约(Contract)不适合您的问题,而 build 商的“组合”可能适合。
我的建议:
class PersonBuilder(private val name: String, private val age: Int) {
fun build() = Person(name, age)
}
class PersonNameBuilder(private val name: String) {
fun withAge(age: Int) = PersonBuilder(name, age)
}
class PersonAgeBuilder(private val age: Int) {
fun withName(name: String) = PersonBuilder(name, age)
}
data class Person(val name: String, val age: Int)
用例:
PersonNameBuilder("Bob").withAge(13).build()
PersonAgeBuilder(25).withName("Claire").build()
PersonNameBuilder("Bob") // can't build(). Forced to add age!
PersonAgeBuilder(25) // can't build(). Forced to add name!
优点:
缺点: