Generics 如何强制客户端代码使用合同初始化Kotlin中所有必需的生成器字段?
在2019年JetBrains开放日,据说Kotlin团队研究了合同,并试图实现只允许在某些上下文中调用函数的上下文合同,例如,只有在调用方法之前一次调用函数Generics 如何强制客户端代码使用合同初始化Kotlin中所有必需的生成器字段?,generics,kotlin,type-inference,contract,Generics,Kotlin,Type Inference,Contract,在2019年JetBrains开放日,据说Kotlin团队研究了合同,并试图实现只允许在某些上下文中调用函数的上下文合同,例如,只有在调用方法之前一次调用函数setName时,才允许调用函数build。这是一段谈话录音 我尝试用当前可用的Kotlin特性模拟这种契约,为数据类Person(val name:String,val age:Int)创建空安全生成器 注意:当然,在这种情况下,使用命名参数而不是生成器模式要容易得多,但是命名参数不允许将未完全构建的对象解析为其他函数,并且当您想要创建由
setName
时,才允许调用函数build
。这是一段谈话录音
我尝试用当前可用的Kotlin特性模拟这种契约,为数据类Person(val name:String,val age:Int)
创建空安全生成器
注意:当然,在这种情况下,使用命名参数而不是生成器模式要容易得多,但是命名参数不允许将未完全构建的对象解析为其他函数,并且当您想要创建由其他复杂对象等组成的复杂对象时,很难使用它们
下面是我的空安全构建器实现:
基于通用标志的生成器
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)
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)
基于合同的建筑商
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)
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)
有没有更好的空安全构建器实现,有没有什么方法可以消除我的实现缺点?我认为契约不适合您的问题,而构建器“组合”可能适合您的问题 我的建议:
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!
优点:
在添加几个字段之前,您的解决方案是很好的。例如,如果您添加
gender
字段,则不仅要添加PersonGenderBuilder
,还要添加PersonNameAgeBuilder
、PersonNameGenderBuilder
和PersonAgeGenderBuilder
。n个字段需要2^n-1个类。这正是我在设计中试图避免的。很多代码都是如此。我想到的另一个解决方法是定义一个映射,该映射将存储字段“set”状态。然后,每个setter都会首先询问map是否已经设置了值。这将产生许多setter,我们无法在运行时看到它,但我们避免使用新类,我不太明白。无论如何,通过使用MutableMap
(或MutableSet
),您只能实现运行时验证,我更希望在不正确使用生成器的情况下获得编译时错误。注意:最好使用属性委托而不是映射。是否需要编译时或运行时检查?