Kotlin DSL-联合结构

时间:2018-11-09 14:22:52

标签: kotlin gradle-kotlin-dsl

我正在设计DSL并遇到一个要求,在该要求中我有一个可以分配给不同方式的变量。大大简化了,我想通过整数或String中的表达式设置value属性。 (真正的需求甚至更加复杂。)

我想写我的DSL:

value = 42

value = "6*7"

该值将在后台存储在DynamicValue<Int>结构中,该结构包含整数或表达式。

class DynamicValue<T>(dv : T?, expr : String) {
    val directValue : T? = dv
    val script : String? = expr
    ...
}

我尝试了几种方法(代理,类等),但是没有一个提供这些语法。

是否有一种方法可以声明这种类似联合的结构?

2 个答案:

答案 0 :(得分:1)

您如何看待以下语法:

value(42)
value("6*7")
//or
value+=42
value+="6*7"

您可以使用操作员功能来做到这一点:

class DynamicValue<T>() {
    var dv: T? = null
    var expr: String? = null

    operator fun invoke(dv : T)  {
        this.dv = dv
        this.expr = null
    }

    operator fun invoke(expr: String)  {
        this.dv = null
        this.expr = expr
    }

    operator fun plusAssign(dv : T)  {
        this.dv = dv
        this.expr = null
    }

    operator fun plusAssign(expr: String)  {
        this.dv = null
        this.expr = expr
    }
}  

您无法在Kotlin中重新定义分配运算符,因此纯语法value=42是不可能的。

但是我不会使用运算符功能,这太神奇了。我会这样做:

val value = DynamicValue<Int>()
value.simple=42
value.expr="6*7"

class DynamicValue2<T>() {
    private var _dv: T? = null
    private var _expr: String? = null
    var simple: T?
        get() = _dv
        set(value) {
            _dv = value
            _expr = null
        }

    var expr: String?
        get() = _expr
        set(value) {
            _expr = value
            _dv = null
        }
}

答案 1 :(得分:0)

Rene的回答给了我领先,最后我想到了这个解决方案。 在此解决方案中,我考虑了所有要求(我在原始问题中提出的要求),因此变得比我原来的问题要复杂得多。

我的全部要求是能够添加在受良好保护的上下文中运行的静态值或脚本(摘要)。这些脚本将被存储,并在以后执行。我想在编写脚本时启用IDE的全部功能,但想保护我的脚本免遭代码注入,并帮助用户仅使用脚本所需的上下文值。

我用来实现此目的的技巧是启用在kotlin中添加脚本,但是在运行整个DSL脚本并创建业务对象之前,我将脚本转换为字符串。 (此字符串稍后将由JSR233引擎在受保护的包装上下文中执行。)此对话迫使我在执行之前先对整个脚本进行标记化,然后搜索/替换一些标记。 (整个令牌生成器和转换器相当长且无聊,因此我将不在此处插入。)

第一种方法

我的目标是能够写出以下任何内容:

myobject {
    value = static { 42 }                // A static solution
    value = static { 6 * 7 }             // Even this is possible
    value = dynamic{ calc(x, y) }        // A pure cotlin solution with IDE support
    value = dynamic("""calc(x * x)""")   // This is the form I convert the above script to
}

其中calcxy在上下文类中定义:

class SpecialScriptContext : ScriptContextBase() {
    val hello = "Hello"
    val x = 29
    val y = 13

    fun calc(x: Int, y: Int) = x + y

    fun greet(name: String) = println("$hello $name!")
}

因此,让我们看看解决方案!首先,我需要一个DynamicValue类来保存其中一个值:

class DynamicValue<T, C : ScriptContextBase, D: ScriptContextDescriptor<C>> 
    private constructor(val directValue: T?, val script: String?) {
    constructor(value: T?) : this(value, null)
    constructor(script: String) : this(null, script)
}

此结构将确保准确设置一个选项(静态,脚本)。 (不必理会C和D类型参数,它们用于基于上下文的脚本支持。)

然后我制作了顶级DSL功能以支持语法:

@PlsDsl
fun <T, C : ScriptContextBase, D : ScriptContextDescriptor<C>> static(block: () -> T): DynamicValue<T, C, D>
        = DynamicValue<T, C, D>(value = block.invoke())

@PlsDsl
fun <T, C : ScriptContextBase, D : ScriptContextDescriptor<C>> dynamic(s: String): DynamicValue<T, C, D>
        = DynamicValue<T, C, D>(script = s)

@PlsDsl
fun <T, C : ScriptContextBase, D : ScriptContextDescriptor<C>> dynamic(block: C.() -> T): DynamicValue<T, C, D> {
    throw IllegalStateException("Can't use this format")
}

对第三种形式的解释。如我之前所写,我不想执行该函数的块。执行脚本时,此形式将转换为字符串形式,因此通常该函数在执行时永远不会出现在脚本中。唯一的例外是健全性警告,永远不会抛出该警告。

最后将该字段添加到我的业务对象构建器中:

@PlsDsl
class MyObjectBuilder {
    var value: DynamicValue<Int, SpecialScriptContext, SpecialScriptContextDescriptor>? = null
}

第二种方法

先前的解决方案有效,但存在一些缺陷:表达式与设置的变量无关,也与设置值的实体无关。通过第二种方法,我解决了此问题,并消除了等号的需要,不必要的花括号。

什么帮助了:扩展函数,中缀函数和密封类。

首先,我将两种值类型拆分为定义了共同祖先的单独的类:

sealed class Value<T, C : ScriptContextBase> {
    abstract val scriptExecutor: ScriptExecutor
    abstract val descriptor: ScriptContextDescriptor<C>
    abstract val code: String
    abstract fun get(context: C): T?
}

class StaticValue<T, C : ScriptContextBase>(override val code: String,
                                                                 override val scriptExecutor: ScriptExecutor,
                                                                 override val descriptor: ScriptContextDescriptor<C>,
                                                                 val value: T? = null
) : Value<T, C>() {
    override fun get(context: C) = value

    constructor(oldValue: Value<T, C>, value: T?) : this(oldValue.code, oldValue.scriptExecutor, oldValue.descriptor, value)
}

class DynamicValue<T, C : ScriptContextBase>(override val code: String,
                                                                  script: String,
                                                                  override val scriptExecutor: ScriptExecutor,
                                                                  override val descriptor: ScriptContextDescriptor<C>)
    : Value<T, C>() {

    constructor(oldValue: Value<T, C>, script: String) : this(oldValue.code, script, oldValue.scriptExecutor, oldValue.descriptor)

    private val scriptCache = scriptExecutor.register(descriptor)
    val source = script?.replace("\\\"\\\"\\\"", "\"\"\"")
    private val compiledScript = scriptCache.register(generateUniqueId(code), source)

    override fun get(context: C): T? = compiledScript.execute<T?>(context)
}

请注意,我将主要构造函数设为内部,并创建了一种复制和更改构造函数。然后,我将新功能定义为公共祖先的扩展并将其标记为infix:

infix fun <T, C : ScriptContextBase> Value<T, C>.static(value: T?): Value<T, C> = StaticValue(this, value)

infix fun <T, C : ScriptContextBase> Value<T, C>.expr(script: String): Value<T, C> = DynamicValue(this, script)

infix fun <T, C : ScriptContextBase> Value<T, C>.dynamic(block: C.() -> T): Value<T, C> {
    throw IllegalStateException("Can't use this format")
}

使用辅助复制和更改构造函数可以继承上下文相关的值。最后,我在DSL构建器中初始化值:

@PlsDsl
class MyDslBuilder {
    var value: Value<Int, SpecialScriptContext> = StaticValue("pl.value", scriptExecutor, SpecialScriptContextDescriptor)
    var value2: Value<Int, SpecialScriptContext> = StaticValue("pl.value2", scriptExecutor, SpecialScriptContextDescriptor)
}

一切就绪,现在我可以在脚本中使用它了:

myobject {
    value static 42
    value2 expr "6 * 7"
    value2 dynamic { calc(x, y) }
}