如何在kotlin DSL构建器中创建字段

red*_*ead 6 kotlin

在Kotlin中,在创建自定义DSL时,在编译时强制填充构建器扩展函数中的必需字段的最佳方法是什么.例如:

person {
    name = "John Doe" // this field needs to be set always, or compile error
    age = 25
}
Run Code Online (Sandbox Code Playgroud)

强制它的一种方法是在函数参数中设置值,而不是扩展函数的主体.

person(name = "John Doe") {
    age = 25
}
Run Code Online (Sandbox Code Playgroud)

但如果有更多必填字段,那就更难以理解了.

还有其他方法吗?

Ban*_*non 11

新的类型推断使您能够创建一个空安全的编译时检查构建器:

data class Person(val name: String, val age: Int?)

// Create a sealed builder class with all the properties that have default values
sealed class PersonBuilder {
    var age: Int? = null // `null` can be a default value if the corresponding property of the data class is nullable

    // For each property without default value create an interface with this property
    interface Named {
        var name: String
    }

    // Create a single private subclass of the sealed class
    // Make this subclass implement all the interfaces corresponding to required properties
    private class Impl : PersonBuilder(), Named {
        override lateinit var name: String // implement required properties with `lateinit` keyword
    }

    companion object {
        // Create a companion object function that returns new instance of the builder
        operator fun invoke(): PersonBuilder = Impl()
    }
}

// For each required property create an extension setter
fun PersonBuilder.name(name: String) {
    contract {
        // In the setter contract specify that after setter invocation the builder can be smart-casted to the corresponding interface type
        returns() implies (this@name is PersonBuilder.Named)
    }
    // To set the property, you need to cast the builder to the type of the interface corresponding to the property
    // The cast is safe since the only subclass of `sealed class PersonBuilder` implements all such interfaces
    (this as PersonBuilder.Named).name = name
}

// Create an extension build function that can only be called on builders that can be smart-casted to all the interfaces corresponding to required properties
// If you forget to put any of these interface into where-clause compiler won't allow you to use corresponding property in the function body
fun <S> S.build(): Person where S : PersonBuilder, S : PersonBuilder.Named = Person(name, age)
Run Code Online (Sandbox Code Playgroud)

用例:

val builder = PersonBuilder() // creation of the builder via `invoke` operator looks like constructor call
builder.age = 25
// builder.build() // doesn't compile because of the receiver type mismatch (builder can't be smart-casted to `PersonBuilder.Named`)
builder.name("John Doe")
val john = builder.build() // compiles (builder is smart-casted to `PersonBuilder & PersonBuilder.Named`)
Run Code Online (Sandbox Code Playgroud)

现在你可以添加一个 DSL 函数:

// Caller must call build() on the last line of the lambda
fun person(init: PersonBuilder.() -> Person) = PersonBuilder().init()
Run Code Online (Sandbox Code Playgroud)

DSL 用例:

person {
    name("John Doe") // will not compile without this line
    age = 25
    build()
}
Run Code Online (Sandbox Code Playgroud)

最后,在 2019 年 JetBrains 开放日,据说 Kotlin 团队研究了合约并试图实施合约,以允许创建具有所需字段的安全 DSL。是俄语的谈话录音。这个特性甚至不是一个实验性的特性,所以它可能永远不会被添加到语言中。


Han*_*aim 5

如果您正在为 Android 进行开发,我编写了一个轻量级 linter 来验证强制 DSL 属性。

要解决您的用例,您只需@DSLMandatoryname属性设置器添加注释,linter 将捕获未分配的任何位置并显示错误:

@set:DSLMandatory
var name: String
Run Code Online (Sandbox Code Playgroud)

呃

您可以在这里查看: https: //github.com/hananrh/dslint/


cma*_*ard 0

很简单,如果块后面的 DLS 中未定义该异常,则抛出异常

fun person(block: (Person) -> Unit): Person {
val p = Person()
block(p)
if (p.name == null) {
  // throw some exception
}
return p
}
Run Code Online (Sandbox Code Playgroud)

或者,如果您想在构建时强制执行它,只需让它在未定义的情况下向外部块返回无用的内容,例如 null。

fun person(block: (Person) -> Unit): Person? {
val p = Person()
block(p)
if (p.name == null) {
  return null
}
return p
}
Run Code Online (Sandbox Code Playgroud)

我猜你会离开这个例子,所以也许地址会是更好的例子:

fun Person.address(block: Address.() -> Unit) {
// city is required
var tempAddress = Address().apply(block)
if (tempAddress.city == null) {
   // throw here
}
}
Run Code Online (Sandbox Code Playgroud)

但是,如果我们想要确保所有内容都已定义,但又想让您以任意顺序执行在编译时中断,该怎么办?很简单,有两种!

data class Person(var name: String = null,
              var age: Int = null,
              var address: Address = null)
data class PersonBuilder(var name: String? = null,
              var age: Int? = null,
              var address: Address? = null)
fun person(block: (PersonBuilder) -> Unit): Person {
    val pb = PersonBuilder()
    block(p)
    val p = Person(pb.name, pb.age, pb.address)
    return p
}
Run Code Online (Sandbox Code Playgroud)

这样,您就可以获得要构建的非严格类型,但最终最好是无空的。这是一个有趣的问题,谢谢。