Kotlin DSL - 联合结构

Kotlin DSL - union structure

我正在设计一个 DSL 和 运行 到一个需求中,我有一个可以分配给不同方式的变量。大大简化了,我想通过整数或字符串中的表达式设置 value 属性 。 (真正的需求更复杂。)

我想用我的 DSL 写:

value = 42

value = "6*7"

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

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

我尝试了几种方法(委托、class 等),但其中 none 提供了这些语法。

有没有办法声明这种联合结构?

你觉得下面的语法怎么样:

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
        }
}

Rene 的回答给了我领先,最后我找到了这个解决方案。 在这个解决方案中,我考虑了我所有的要求(我在原来的问题中删除的那些)所以这变得比我原来的问题所需要的要复杂得多。

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

我用来实现这个的技巧是在 kotlin 中启用添加脚本,但在我 运行 整个 DSL 脚本和创建业务对象之前,我将脚本转换为字符串。 (此字符串稍后将由 JSR233 引擎在受保护的包装上下文中执行。)此对话迫使我在执行之前对整个脚本和 search/replace 一些标记进行标记。 (整个tokenizer和converter比较冗长乏味,这里就不插了。)

第一种方法

我的目标是能够写下这些:

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:

中定义
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 来保存其中一个值:

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
}

第二种方法

以前的解决方案有效但有一些缺陷:表达式与其设置的变量无关,也与设置值的实体无关。使用第二种方法我解决了这个问题并消除了等号的需要和大多数不必要的大括号。

有什么帮助:扩展函数、中缀函数和密封的 classes.

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

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