代码之家  ›  专栏  ›  技术社区  ›  redhead

如何在kotlin DSL builders中创建所需的字段

  •  6
  • redhead  · 技术社区  · 7 年前

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

    person {
        name = "John Doe" // this field needs to be set always, or compile error
        age = 25
    }
    

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

    person(name = "John Doe") {
        age = 25
    }
    

    但如果有更多必填字段,则会使其更难阅读。

    还有别的办法吗?

    1 回复  |  直到 7 年前
        1
  •  12
  •   IlyaMuravjov    5 年前

    New type inference 允许您创建空安全编译时检查生成器:

    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)
    

    用例:

    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`)
    

    现在,您可以添加DSL功能:

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

    DSL用例:

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

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

        2
  •  4
  •   Hanan Rofe Haim    6 年前

    如果你是为Android开发的,我写了一个轻量级的linter来验证强制性的DSL属性。

    要解决您的用例,您只需要添加注释 @DSLMandatory 给你的 name 属性设置器和过梁将捕捉未分配的任何位置,并显示错误:

    @set:DSLMandatory
    var name: String
    

    err

    你可以看看这里: https://github.com/hananrh/dslint/

        3
  •  0
  •   cmaynard    7 年前

    简单地说,如果在数据块之后DLS中没有定义异常,则抛出异常

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

    或者,如果您想在构建时强制执行它,只要让它在没有定义的情况下向外部块返回一些无用的东西,比如null。

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

    我猜你要走了 this example 因此,地址可能是更好的例子:

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

    但是,如果我们想确保所有事情都被定义好了,但又想让你按任何顺序去做呢 在编译时中断。简单,有两种类型!

    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
    }
    

    通过这种方式,您可以得到要构建的非严格类型,但最好在最后减少null。这是个有趣的问题,谢谢。