跳到主要内容

Kotlin DSL

问题

什么是 Kotlin DSL?如何利用 Kotlin 语法特性构建类型安全的 DSL?

答案

1. DSL 概念

DSL(Domain Specific Language,领域特定语言)是针对特定领域设计的编程语言或 API 风格。Kotlin 的语法特性(Lambda、扩展函数、中缀函数、运算符重载)使其天然适合构建 DSL。

Android 开发中的 DSL 示例:Gradle Kotlin DSL、Jetpack Compose、Anko、Coil 配置等。

2. DSL 核心语法特性

带接收者的 Lambda

// 普通 Lambda
val greet: (String) -> String = { name -> "Hello, $name" }

// 带接收者的 Lambda —— DSL 的核心
val greet: String.() -> String = { "Hello, $this" }
"World".greet() // "Hello, World"

这正是 applywith 等作用域函数的原理:

// apply 的签名:fun <T> T.apply(block: T.() -> Unit): T
// block 的接收者是 T,所以 block 内可以直接访问 T 的成员

中缀函数

infix fun Int.times(str: String) = str.repeat(this)
println(2 times "hello ") // "hello hello "

// Pair 的 to 就是中缀函数
val pair = "key" to "value" // Pair("key", "value")

运算符重载

data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point) = Point(x + other.x, y + other.y)
}

Point(1, 2) + Point(3, 4) // Point(4, 6)

3. 构建类型安全 DSL

以构建 HTML 为例展示如何设计 DSL:

// 1. 定义 DSL 的节点类
@DslMarker
annotation class HtmlDsl

@HtmlDsl
open class Tag(val name: String) {
val children = mutableListOf<Tag>()
val attributes = mutableMapOf<String, String>()

protected fun <T : Tag> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}

override fun toString(): String {
val attrs = if (attributes.isEmpty()) ""
else attributes.entries.joinToString(" ", prefix = " ") { "${it.key}=\"${it.value}\"" }
val content = children.joinToString("")
return "<$name$attrs>$content</$name>"
}
}

class Html : Tag("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}

class Head : Tag("head") {
fun title(text: String) {
children.add(object : Tag("title") {
override fun toString() = "<title>$text</title>"
})
}
}

class Body : Tag("body") {
fun div(init: Div.() -> Unit) = initTag(Div(), init)
fun p(text: String) = initTag(P(text)) {}
}

class Div : Tag("div")
class P(text: String) : Tag("p")

// 2. 入口函数
fun html(init: Html.() -> Unit): Html {
val html = Html()
html.init()
return html
}

// 3. 使用 DSL
val page = html {
head {
title("My Page")
}
body {
div {
p("Hello, DSL!")
}
}
}

4. Android 中的常见 DSL

Gradle Kotlin DSL

// build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}

android {
compileSdk = 34
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 34
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android.txt"))
}
}
}

dependencies {
implementation("androidx.core:core-ktx:1.12.0")
testImplementation("junit:junit:4.13.2")
}

Jetpack Compose(声明式 UI DSL)

@Composable
fun UserCard(user: User) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = user.name, style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(8.dp))
Text(text = user.email, style = MaterialTheme.typography.bodyMedium)
}
}
}

自定义 RecyclerView Adapter DSL

// DSL 定义
class AdapterDsl<T> {
var items: List<T> = emptyList()
private var _layoutId: Int = 0
private var _bind: (View, T, Int) -> Unit = { _, _, _ -> }

fun layout(id: Int) { _layoutId = id }
fun bind(block: (View, T, Int) -> Unit) { _bind = block }

fun build(): RecyclerView.Adapter<*> = SimpleAdapter(items, _layoutId, _bind)
}

fun <T> RecyclerView.setup(init: AdapterDsl<T>.() -> Unit) {
val dsl = AdapterDsl<T>().apply(init)
adapter = dsl.build()
}

// 使用
recyclerView.setup<User> {
items = userList
layout(R.layout.item_user)
bind { view, user, position ->
view.findViewById<TextView>(R.id.name).text = user.name
}
}

5. @DslMarker 注解

@DslMarker 防止 DSL 嵌套作用域中意外访问外层接收者:

@DslMarker
annotation class HtmlDsl

@HtmlDsl
class Table {
fun tr(init: Tr.() -> Unit) { /* ... */ }
}

@HtmlDsl
class Tr {
fun td(text: String) { /* ... */ }
}

// 没有 @DslMarker 时:
table {
tr {
tr { } // ⚠️ 意外调用了外层 table 的 tr(不报错但语义错误)
}
}

// 有 @DslMarker 时:
table {
tr {
tr { } // ❌ 编译错误!必须用 this@table.tr { } 显式调用
}
}

常见面试问题

Q1: Kotlin DSL 的实现原理是什么?

答案

Kotlin DSL 的核心是带接收者的 Lambda + 扩展函数

  1. 带接收者的 Lambda T.() -> Unit 使 Lambda 内部可以直接访问 T 的成员
  2. 扩展函数为已有类添加新方法
  3. 编译器会进行类型检查,保证 DSL 的类型安全
  4. Lambda 的最后一个参数可以放到括号外(尾随 Lambda),使语法更自然

Q2: Compose 是如何暴露 DSL 风格 API 的?

答案

Compose 的 @Composable 函数本质上是带有隐式参数 $composer 的函数。通过 Kotlin 编译器插件在编译时注入参数。嵌套调用形成组合树,编译器保证只能在 @Composable 作用域内调用 @Composable 函数。

// Compose 的 Column 签名
@Composable
fun Column(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit // 带接收者的 Composable Lambda
)

ColumnScope 作为接收者,限定了 content 内只能使用 Column 允许的 API(如 Modifier.weight())。

Q3: 如何防止 DSL 嵌套中的作用域泄漏?

答案

使用 @DslMarker 注解标记 DSL 的所有构建器类。编译器会阻止在内层作用域隐式访问外层接收者的成员,必须通过 this@外层 显式访问。这防止了代码中难以发现的逻辑错误。

相关链接