【问题标题】:How to enforce usage rules on Custom Kotlin DSL如何在自定义 Kotlin DSL 上实施使用规则
【发布时间】:2020-05-23 10:08:13
【问题描述】:

我正在按照以下示例调查 Kotlin DSL:-

https://github.com/zsmb13/VillageDSL

我对如何对 DSL 公开的所有属性强制执行使用规则感兴趣。

举个例子:-

val v = village {
    house {
        person {
            name = "Emily"
            age = 31
        }
         person {
            name = "Jane"
            age = 19
        }
    }
}

我想强制执行一个规则,阻止 DSL 的用户输入重复的属性,如下所示

val v = village {
    house {
        person {
            name = "Emily"
            name = "Elizabeth"
            age = 31
        }
         person {
            name = "Jane"
            age = 19
            age = 56
        }
    }
}

我已经尝试过使用 Kotlin 合约,例如

contract { callsInPlace(block, EXACTLY_ONCE) }

但是,这些只允许在顶级函数中使用,并且在遵循 DSL 中的 Builder 模式时,我看不到如何使用合约,例如

@SimpleDsl1
class PersonBuilder(initialName: String, initialAge: Int) {
    var name: String = initialName
    var age: Int = initialAge

    fun build(): Person {
        return Person(name, age)
    }
}

是否可以达到我想要的效果,即每个人只强制设置一个属性?

【问题讨论】:

标签: kotlin dsl


【解决方案1】:

很遗憾,您无法使用合约来获取您正在寻找的编译错误。我不认为它们是为了你在这里绑定的目的......但我可能错了。对我来说,它们是对编译器关于可空性和不变性等事情的提示。即使您能够按照自己的意愿使用它们,我认为您不会得到您正在寻找的编译错误。

但第二个解决方案是在运行时出现异常。属性委托可以为您提供一个很好的可重用解决方案。这是对您的示例进行了一些修改。

class PersonBuilder {
    var name: String? by OnlyOnce(null)
    var age: Int? by OnlyOnce(null)

    fun build(): Person {
        name?.let { name ->
            age?.let { age ->
                return Person(name, age)
            }
        }
        throw Exception("Values not set")
    }
}

class OnlyOnce<V>(initialValue: V) {

    private var internalValue: V = initialValue
    private var set: Boolean = false

    operator fun getValue(thisRef: Any?, property: KProperty<*>): V {
        return internalValue
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: V) {
        if (set) {
            throw Exception("Value set already")
        }
        this.internalValue = value
        this.set = true
    }
}

fun person(body: PersonBuilder.() -> Unit) {
    //do what you want with result
    val builder = PersonBuilder()
    builder.body()
}

fun main() {
    person {
        name = "Emily"
        age = 21
        age = 21 // Exception thrown here
    }
}

【讨论】:

    【解决方案2】:

    我找到了一种做类似事情的 hacky 方法,但后来发现中缀函数在这里不起作用,因为 this bug。当它得到修复时,这个解决方案应该没问题。

    您可以使您的 DSL 看起来像这样,但不幸的是,您的第一个 set 调用不能是中缀 :( 因为那时 name 不能智能转换为 SetProperty&lt;*&gt;(请参阅上面的错误报告)。

    val emily = person {
            name.set("Emily")
            name.set("Elizabeth") //Error here
            age.set(31)
            age set 90 //Won't work either
        }
    

    弹出的错误(对于name.set("Elizabeth"))是:

    Type inference failed: Cannot infer type parameter T in inline infix fun <reified T> Property<T>.set(t: T): Unit
    None of the following substitutions
    receiver: Property<CapturedTypeConstructor(out Any?)>  arguments: (CapturedTypeConstructor(out Any?))
    receiver: Property<String>  arguments: (String)
    can be applied to
    receiver: UnsetProperty<String>  arguments: (String)
    

    背后的代码:

    @OptIn(ExperimentalContracts::class)
    infix fun <T> Property<T>.set(t: T){
        contract { returns() implies (this@set is Prop<*>) }
        this.setData(t)
    }
    
    interface Property<T> {
        fun data(): T
        fun setData(t: T)
    }
    
    interface UnsetProperty<T> : Property<T>
    
    open class SetProperty<T>(val name: String) : Property<T> {
        private var _data: T? = null
        override fun data(): T { return _data ?: throw Error("$name not defined") }
        override fun setData(t: T) {
            if (_data == null) _data = t
            else throw Error("$name already defined")
        }
    }
    
    class Prop<T>(name: String = "<unnamed property>") : SetProperty<T>(name), UnsetProperty<T>
    
    class PersonBuilder {
        val name: Property<String> = Prop("name")
        val age: Property<Int> = Prop("age")
        fun build(): Person = Person(name.data(), age.data())
    }
    
    fun person(f: PersonBuilder.() -> Unit): Person {
        val builder = PersonBuilder()
        builder.f()
        return builder.build()
    }
    
    data class Person(val name: String, val age: Int)
    

    我不确定这到底为什么有效/无效,但似乎因为TProperty 中是不变的,它无法确定它到底是什么。


    但是,为 person 函数使用命名参数并让 housevillage 等具有可变参数会更容易、更安全。

    【讨论】:

    • 这与我建议的方法非常相似,使用属性引用::foo,但一个错误也阻止了这种情况:youtrack.jetbrains.com/issue/KT-17018
    • @BenjaminCharais 我很想看看您的方法,因为我不确定该错误与此有何关系。你能发布答案吗?
    • 最大的不同是不是让一切都通过 SetProperty,该模式将允许对属性进行动态解释,因此修改类本身会相对轻松,并且看起来与普通代码实现非常相似,其他一切或多或少都是一样的。稍后我可以提出一个存根示例。 -- 编辑 -- 我会写它来处理异常,这样它是一个可行的答案,并且可能会有所帮助
    【解决方案3】:

    在写这篇文章的时候,我注意到我在 cmets 中引用的 Bug 已经被修复了,所以我继续沿着这条代码路径走下去才意识到存在语言限制,在底部我将包含一个示例我的意思是在 cmets 中。

    这两个例子在运行时都会失败。

        fun test_person() {
        val village = village {
            house {
                person {
                    name = "Emily"
        //            ::name setTo "Emily" // Commented for 2nd example of Person
                    age = 31
                }
                person {
                    name = "Jane"
                    age = 19
                }
            }
            house {
                person {
                    name = "Tim"
         //           name = "Tom" // Will break with exception
                    age = 20
                }
            }
        }
    
        println("What is our village: \n$village")
    }
    

    适用于异常的运行时中断示例:

    class Village {
        val houses = mutableListOf<House>()
    
        fun house(people: House.() -> Unit) {
            val house = House()
            house.people()
            houses.add(house)
        }
    
        override fun toString(): String {
            val strB = StringBuilder("Village:\n")
            houses.forEach { strB.append("  $it \n") }
            return strB.toString()
        }
    }
    
    fun village(houses: Village.() -> Unit): Village  {
        val village = Village()
        village.houses()
        return village
    }
    
    class House {
        val people = mutableListOf<Person>()
    
        fun person(qualities: Person.() -> Unit) {
            val person = Person()
            person.qualities()
            people.add(person)
        }
    
        override fun toString(): String {
            val strB = StringBuilder("House:\n")
            people.forEach{ strB.append("    $it \n")}
            return strB.toString()
        }
    }
    
    class Person {
        var age by SetOnce<Int>()
        var name by SetOnce<String>()
    
        override fun toString(): String {
            return "Person: { Name: $name, Age: $age }"
        }
    }
    
    class SetOnce <T> : ReadWriteProperty<Any?, T?> {
        private var default: T? = null
    
        override fun getValue(thisRef: Any?, property: KProperty<*>): T? = default
    
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
            if (default != null) throw Exception("Duplicate set for ${property.name} on $thisRef")
            else default = value
        }
    }
    

    非工作示例旨在使用 lateinit 属性来控制一次值的设置,但您不能使用引用,它必须是文字 ::foo 语法。就像我说的,我没有意识到这个错误已经修复,也不知道这是语言的限制

    class Person {
        lateinit var name: String
        lateinit var age: Number // Number because primitives can't be lateinit
    
        /** @throws Exception when setting a property a second time */
        infix fun <T> KMutableProperty0<T>.setTo(value: T) {
            val prop = getProp<T>(this.name)
    
            if (prop.isInitialized.not()) this.set(value)
            else throw Exception("Duplicate set for ${this.name}")
        }
    
        private fun <T> getProp(name: String): KMutableProperty0<T> {
    
            return when(name) {
                "name" -> ::name
                "age" -> ::age
                else -> throw Exception("Non-existent property: $name")
            } as KMutableProperty0<T>
        }
    }
    

    随着合同的成熟和规则的放松,我们可能会写出类似的东西:

    @OptIn(ExperimentalContracts::class)
    infix fun <T> KMutableProperty0<T>.setTo(value: T) {
        contract { returns() implies this@setTo.isInitialized }
        this.set(value)
    }
    

    这将使我们能够将这一切都转移到 IDE 错误中,但遗憾的是我们还没有做到。

    【讨论】:

      猜你喜欢
      • 2011-01-20
      • 2018-02-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-08-03
      • 2016-05-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多