且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

如何在Kotlin DSL构建器中创建必填字段

更新时间:2023-01-08 23:36:42

通过新类型推断,您可以制作一个空安全的编译时检查生成器:

New type inference enables you to make a null-safe compile-time checked builder:

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功能:

Now you can add a DSL function:

// 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. 此处是俄语的谈话录音.此功能甚至还不是实验性功能,因此 也许永远不会将其添加到语言中.


Finally, on JetBrains open day 2019 it was said that the Kotlin team researched contracts and tried to implement contracts that will allow creating safe DSL with required fields. Here is a talk recording in Russian. This feature isn't even an experimental one, so maybe it will never be added to the language.