You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin DSL能否通过编译器强制执行函数调用相关编译约束?

Kotlin DSL编译期约束实现方案(针对HTML构建器场景)

可以通过基于状态的类型演变实现你要的三个编译期约束,DslMarker仅用于控制DSL作用域,不处理调用状态,所以得靠Kotlin静态类型系统跟踪上下文状态来实现。以下针对每个需求给出具体方案:

1. 强制执行调用顺序(先head后body)

核心是让DSL的上下文类型随调用步骤动态变化,不同状态的上下文只暴露允许调用的方法:

// 初始上下文:仅允许调用head
class HtmlInitialContext {
    fun head(block: Head.() -> Unit): HtmlAfterHeadContext {
        // 实际处理head节点逻辑
        return HtmlAfterHeadContext()
    }
}

// 调用head后的上下文:仅允许调用body
class HtmlAfterHeadContext {
    fun body(block: Body.() -> Unit) {
        // 实际处理body节点逻辑
    }
}

// DSL入口:初始上下文为HtmlInitialContext
fun html(block: HtmlInitialContext.() -> Unit) {
    HtmlInitialContext().apply(block)
}

// 错误用法:先调用body会直接编译报错
html {
    body { } // 编译错误:Unresolved reference: body
    head { }
}

// 正确用法
html {
    head { }
    body { }
}

2. 禁止重复调用(如head只能调用一次)

同样利用状态切换,调用目标方法后切换到无该方法的上下文类型,从根源上阻止重复调用:

// 扩展一下head内部的约束:title只能调用一次
class HeadInitialContext {
    fun title(text: String): HeadAfterTitleContext {
        // 处理title逻辑
        return HeadAfterTitleContext()
    }
}

// 调用title后的head上下文:不再提供title方法
class HeadAfterTitleContext

// 修改head函数,使用状态式上下文
fun HtmlInitialContext.head(block: HeadInitialContext.() -> Unit): HtmlAfterHeadContext {
    HeadInitialContext().apply(block)
    return HtmlAfterHeadContext()
}

// 错误用法:重复调用title编译报错
html {
    head {
        title("Hello")
        title("World") // 编译错误:Unresolved reference: title
    }
    body { }
}

3. 要求必需调用(如必须调用body)

通过让DSL入口函数要求块返回一个"完成状态"的类型,而这个类型只能通过调用必需方法获得:

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

// 标记DSL完成的类型
class CompletedHtmlContext

class HtmlAfterHeadContext {
    fun body(block: Body.() -> Unit): CompletedHtmlContext {
        // 处理body逻辑
        return CompletedHtmlContext()
    }
}

// 用contract强化编译期检查(需开启ExperimentalContracts)
@OptIn(ExperimentalContracts::class)
inline fun html(block: HtmlInitialContext.() -> CompletedHtmlContext) {
    contract {
        callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
    }
    HtmlInitialContext().run(block)
}

// 错误用法:未调用body,无法返回CompletedHtmlContext,编译报错
html {
    head { }
    // 编译错误:Type mismatch: inferred type is Unit but CompletedHtmlContext was expected
}

// 正确用法
html {
    head { }
    body { }
}

补充说明

DslMarker的作用是限制DSL内部只能访问当前上下文的方法,避免外部同名函数干扰,但它不具备状态跟踪能力。而上面的状态式类型控制完全依赖Kotlin静态类型检查,所有约束在编译阶段就会触发错误,不需要运行时抛异常。

内容的提问来源于stack exchange,提问作者user26543933

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 13:33:28