Kotlin基础知识点 #149 DSL构建基础
DSL(Domain Specific Language,领域特定语言)是为特定领域设计的小型语言。Kotlin的语法特性(lambda with receiver、中缀函数、操作符重载等)使它非常适合构建DSL,让代码更接近自然语言,提高可读性。:DSL是Kotlin的强大特性,合理使用可以让代码更优雅、更易读。Lambda with receiver是构建DSL的核心技术。
·
Kotlin基础知识点 #149 DSL构建基础
难度等级:⭐⭐⭐
🎯 问题背景
DSL(Domain Specific Language,领域特定语言)是为特定领域设计的小型语言。Kotlin的语法特性(lambda with receiver、中缀函数、操作符重载等)使它非常适合构建DSL,让代码更接近自然语言,提高可读性。
// 传统方式:构建HTML
val html = StringBuilder()
html.append("<html>")
html.append("<body>")
html.append("<h1>Hello</h1>")
html.append("<p>World</p>")
html.append("</body>")
html.append("</html>")
// DSL方式:更直观和类型安全
val html = html {
body {
h1 { +"Hello" }
p { +"World" }
}
}
💡 核心概念
1. Lambda with Receiver
Lambda with receiver是构建DSL的核心技术。
// 普通lambda
fun buildString(builder: (StringBuilder) -> Unit): String {
val sb = StringBuilder()
builder(sb)
return sb.toString()
}
val result1 = buildString { sb ->
sb.append("Hello ")
sb.append("World")
}
// Lambda with receiver
fun buildStringDsl(builder: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.builder()
return sb.toString()
}
val result2 = buildStringDsl {
append("Hello ")
append("World")
// this 是 StringBuilder 实例
}
2. 简单的DSL示例
// 构建用户对象的DSL
class User {
var name: String = ""
var age: Int = 0
var email: String = ""
fun address(builder: Address.() -> Unit) {
this.address = Address().apply(builder)
}
var address: Address? = null
}
class Address {
var street: String = ""
var city: String = ""
var country: String = ""
}
fun user(builder: User.() -> Unit): User {
return User().apply(builder)
}
// 使用DSL
val user = user {
name = "Alice"
age = 25
email = "alice@example.com"
address {
street = "123 Main St"
city = "New York"
country = "USA"
}
}
3. HTML DSL
// HTML元素基类
@DslMarker
annotation class HtmlDsl
@HtmlDsl
abstract class Element(val name: String) {
protected val children = mutableListOf<Element>()
protected val attributes = mutableMapOf<String, String>()
fun attribute(name: String, value: String) {
attributes[name] = value
}
fun render(indent: String = ""): String {
val sb = StringBuilder()
sb.append("$indent<$name")
if (attributes.isNotEmpty()) {
attributes.forEach { (key, value) ->
sb.append(" $key=\"$value\"")
}
}
if (children.isEmpty()) {
sb.append(" />")
} else {
sb.append(">\n")
children.forEach { child ->
sb.append(child.render("$indent "))
}
sb.append("$indent</$name>\n")
}
return sb.toString()
}
}
// 文本节点
class TextElement(val text: String) : Element("") {
override fun render(indent: String): String = "$indent$text\n"
}
// HTML元素
class Html : Element("html") {
fun head(builder: Head.() -> Unit) {
children.add(Head().apply(builder))
}
fun body(builder: Body.() -> Unit) {
children.add(Body().apply(builder))
}
}
class Head : Element("head") {
fun title(builder: Title.() -> Unit) {
children.add(Title().apply(builder))
}
}
class Title : Element("title") {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
class Body : Element("body") {
fun h1(builder: H1.() -> Unit) {
children.add(H1().apply(builder))
}
fun p(builder: P.() -> Unit) {
children.add(P().apply(builder))
}
fun div(builder: Div.() -> Unit) {
children.add(Div().apply(builder))
}
}
class H1 : Element("h1") {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
class P : Element("p") {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
class Div : Element("div") {
fun p(builder: P.() -> Unit) {
children.add(P().apply(builder))
}
}
// DSL入口函数
fun html(builder: Html.() -> Unit): Html {
return Html().apply(builder)
}
// 使用示例
fun main() {
val page = html {
head {
title { +"My Page" }
}
body {
h1 { +"Welcome" }
div {
p { +"This is a paragraph" }
p { +"This is another paragraph" }
}
}
}
println(page.render())
}
4. @DslMarker:防止作用域泄漏
// 问题:没有@DslMarker时,可以访问外层作用域
class Outer {
fun outerMethod() {}
inner class Inner {
fun innerMethod() {}
fun test() {
outerMethod() // 可以访问外层方法
}
}
}
// 解决:使用@DslMarker防止作用域混淆
@DslMarker
annotation class MyDsl
@MyDsl
class SafeOuter {
fun outerMethod() {}
fun inner(builder: SafeInner.() -> Unit) {
SafeInner().builder()
}
}
@MyDsl
class SafeInner {
fun innerMethod() {}
fun test() {
innerMethod() // ✅ 可以访问
// outerMethod() // ❌ 编译错误:不能访问外层
}
}
代码示例:实战应用
1. SQL DSL
@DslMarker
annotation class SqlDsl
@SqlDsl
class SelectBuilder {
private val columns = mutableListOf<String>()
private var fromTable: String = ""
private val whereClauses = mutableListOf<String>()
private val orderByClauses = mutableListOf<String>()
private var limitValue: Int? = null
fun select(vararg columns: String) {
this.columns.addAll(columns)
}
fun from(table: String) {
this.fromTable = table
}
fun where(clause: String) {
whereClauses.add(clause)
}
infix fun String.eq(value: Any) {
where("$this = ${formatValue(value)}")
}
infix fun String.gt(value: Any) {
where("$this > ${formatValue(value)}")
}
infix fun String.lt(value: Any) {
where("$this < ${formatValue(value)}")
}
fun orderBy(column: String, direction: String = "ASC") {
orderByClauses.add("$column $direction")
}
fun limit(value: Int) {
this.limitValue = value
}
fun build(): String {
val sb = StringBuilder()
// SELECT
val columnList = if (columns.isEmpty()) "*" else columns.joinToString(", ")
sb.append("SELECT $columnList")
// FROM
if (fromTable.isNotEmpty()) {
sb.append(" FROM $fromTable")
}
// WHERE
if (whereClauses.isNotEmpty()) {
sb.append(" WHERE ${whereClauses.joinToString(" AND ")}")
}
// ORDER BY
if (orderByClauses.isNotEmpty()) {
sb.append(" ORDER BY ${orderByClauses.joinToString(", ")}")
}
// LIMIT
limitValue?.let {
sb.append(" LIMIT $it")
}
return sb.toString()
}
private fun formatValue(value: Any): String {
return when (value) {
is String -> "'$value'"
else -> value.toString()
}
}
}
fun query(builder: SelectBuilder.() -> Unit): String {
return SelectBuilder().apply(builder).build()
}
// 使用示例
fun main() {
val sql1 = query {
select("name", "age", "email")
from("users")
"age" gt 18
"name" eq "Alice"
orderBy("age", "DESC")
limit(10)
}
println(sql1)
// SELECT name, age, email FROM users WHERE age > 18 AND name = 'Alice' ORDER BY age DESC LIMIT 10
val sql2 = query {
from("products")
"price" lt 100
orderBy("price")
}
println(sql2)
// SELECT * FROM products WHERE price < 100 ORDER BY price ASC
}
2. JSON DSL
@DslMarker
annotation class JsonDsl
@JsonDsl
sealed class JsonValue {
data class JsonString(val value: String) : JsonValue()
data class JsonNumber(val value: Number) : JsonValue()
data class JsonBoolean(val value: Boolean) : JsonValue()
object JsonNull : JsonValue()
data class JsonArray(val values: List<JsonValue>) : JsonValue()
data class JsonObject(val properties: Map<String, JsonValue>) : JsonValue()
fun toJsonString(): String {
return when (this) {
is JsonString -> "\"$value\""
is JsonNumber -> value.toString()
is JsonBoolean -> value.toString()
is JsonNull -> "null"
is JsonArray -> values.joinToString(", ", "[", "]") { it.toJsonString() }
is JsonObject -> properties.entries.joinToString(
", ",
"{",
"}"
) { "\"${it.key}\": ${it.value.toJsonString()}" }
}
}
}
@JsonDsl
class JsonObjectBuilder {
private val properties = mutableMapOf<String, JsonValue>()
infix fun String.to(value: String) {
properties[this] = JsonValue.JsonString(value)
}
infix fun String.to(value: Number) {
properties[this] = JsonValue.JsonNumber(value)
}
infix fun String.to(value: Boolean) {
properties[this] = JsonValue.JsonBoolean(value)
}
infix fun String.to(value: JsonValue) {
properties[this] = value
}
fun obj(name: String, builder: JsonObjectBuilder.() -> Unit) {
properties[name] = JsonObjectBuilder().apply(builder).build()
}
fun array(name: String, builder: JsonArrayBuilder.() -> Unit) {
properties[name] = JsonArrayBuilder().apply(builder).build()
}
fun build(): JsonValue.JsonObject = JsonValue.JsonObject(properties)
}
@JsonDsl
class JsonArrayBuilder {
private val values = mutableListOf<JsonValue>()
operator fun String.unaryPlus() {
values.add(JsonValue.JsonString(this))
}
operator fun Number.unaryPlus() {
values.add(JsonValue.JsonNumber(this))
}
operator fun Boolean.unaryPlus() {
values.add(JsonValue.JsonBoolean(this))
}
operator fun JsonValue.unaryPlus() {
values.add(this)
}
fun obj(builder: JsonObjectBuilder.() -> Unit) {
values.add(JsonObjectBuilder().apply(builder).build())
}
fun build(): JsonValue.JsonArray = JsonValue.JsonArray(values)
}
fun json(builder: JsonObjectBuilder.() -> Unit): JsonValue.JsonObject {
return JsonObjectBuilder().apply(builder).build()
}
// 使用示例
fun main() {
val userData = json {
"id" to "001"
"name" to "Alice"
"age" to 25
"active" to true
obj("address") {
"street" to "123 Main St"
"city" to "New York"
}
array("tags") {
+"kotlin"
+"android"
+"developer"
}
array("scores") {
+95
+87
+92
}
array("friends") {
obj {
"name" to "Bob"
"age" to 28
}
obj {
"name" to "Charlie"
"age" to 30
}
}
}
println(userData.toJsonString())
}
3. 配置DSL
@DslMarker
annotation class ConfigDsl
@ConfigDsl
class ServerConfig {
var host: String = "localhost"
var port: Int = 8080
var ssl: Boolean = false
fun database(builder: DatabaseConfig.() -> Unit) {
this.database = DatabaseConfig().apply(builder)
}
var database: DatabaseConfig? = null
fun logging(builder: LoggingConfig.() -> Unit) {
this.logging = LoggingConfig().apply(builder)
}
var logging: LoggingConfig? = null
}
@ConfigDsl
class DatabaseConfig {
var url: String = ""
var username: String = ""
var password: String = ""
var maxConnections: Int = 10
}
@ConfigDsl
class LoggingConfig {
var level: String = "INFO"
var file: String = "app.log"
var format: String = "json"
}
fun server(builder: ServerConfig.() -> Unit): ServerConfig {
return ServerConfig().apply(builder)
}
// 使用示例
fun main() {
val config = server {
host = "api.example.com"
port = 443
ssl = true
database {
url = "jdbc:postgresql://localhost:5432/mydb"
username = "admin"
password = "secret"
maxConnections = 20
}
logging {
level = "DEBUG"
file = "/var/log/app.log"
format = "json"
}
}
println("服务器配置:")
println(" Host: ${config.host}:${config.port}")
println(" SSL: ${config.ssl}")
println(" Database: ${config.database?.url}")
println(" Log Level: ${config.logging?.level}")
}
4. 测试断言DSL
@DslMarker
annotation class AssertDsl
@AssertDsl
class Assertion<T>(private val actual: T) {
infix fun shouldBe(expected: T) {
if (actual != expected) {
throw AssertionError("期望 $expected,实际 $actual")
}
}
infix fun shouldNotBe(expected: T) {
if (actual == expected) {
throw AssertionError("不应该等于 $expected")
}
}
fun shouldBeNull() {
if (actual != null) {
throw AssertionError("应该为null,实际 $actual")
}
}
fun shouldNotBeNull() {
if (actual == null) {
throw AssertionError("不应该为null")
}
}
}
@AssertDsl
class NumberAssertion<T : Number>(private val actual: T) {
infix fun shouldBeGreaterThan(expected: T) {
if (actual.toDouble() <= expected.toDouble()) {
throw AssertionError("$actual 应该大于 $expected")
}
}
infix fun shouldBeLessThan(expected: T) {
if (actual.toDouble() >= expected.toDouble()) {
throw AssertionError("$actual 应该小于 $expected")
}
}
infix fun shouldBeInRange(range: ClosedRange<T>) {
if (actual.toDouble() !in range.start.toDouble()..range.endInclusive.toDouble()) {
throw AssertionError("$actual 应该在范围 $range 内")
}
}
}
@AssertDsl
class StringAssertion(private val actual: String) {
infix fun shouldContain(substring: String) {
if (!actual.contains(substring)) {
throw AssertionError("\"$actual\" 应该包含 \"$substring\"")
}
}
infix fun shouldStartWith(prefix: String) {
if (!actual.startsWith(prefix)) {
throw AssertionError("\"$actual\" 应该以 \"$prefix\" 开头")
}
}
infix fun shouldEndWith(suffix: String) {
if (!actual.endsWith(suffix)) {
throw AssertionError("\"$actual\" 应该以 \"$suffix\" 结尾")
}
}
fun shouldBeEmpty() {
if (actual.isNotEmpty()) {
throw AssertionError("字符串应该为空,实际: \"$actual\"")
}
}
}
fun <T> T.should(): Assertion<T> = Assertion(this)
fun <T : Number> T.shouldNum(): NumberAssertion<T> = NumberAssertion(this)
fun String.shouldStr(): StringAssertion = StringAssertion(this)
// 使用示例
fun main() {
// 基本断言
val result = 42
result.should() shouldBe 42
// 数字断言
100.shouldNum() shouldBeGreaterThan 50
50.shouldNum() shouldBeLessThan 100
75.shouldNum() shouldBeInRange 50..100
// 字符串断言
"Hello World".shouldStr() shouldContain "World"
"Hello World".shouldStr() shouldStartWith "Hello"
"Hello World".shouldStr() shouldEndWith "World"
// Null检查
val nullable: String? = null
nullable.should().shouldBeNull()
val notNull: String? = "value"
notNull.should().shouldNotBeNull()
println("所有测试通过!")
}
⚡ 关键要点
- Lambda with receiver:DSL的核心,让代码更自然
- @DslMarker:防止作用域泄漏,提高类型安全
- 中缀函数:让DSL更接近自然语言
- 操作符重载:增强DSL的表达能力
- 类型安全:Kotlin的类型系统保证DSL的正确性
最佳实践
// ✅ 使用@DslMarker防止作用域混淆
@DslMarker
annotation class MyDsl
// ✅ 提供清晰的DSL入口函数
fun html(builder: Html.() -> Unit): Html = Html().apply(builder)
// ✅ 使用有意义的函数名
fun user { ... } // ✅ 清晰
fun create { ... } // ❌ 不明确
// ✅ 提供默认值
class Config {
var timeout: Int = 30 // 有默认值
var url: String = "" // 必填,但有默认值避免null
}
// ✅ 使用中缀函数提高可读性
"age" eq 18 // ✅ 清晰
eq("age", 18) // ❌ 不如中缀形式
// ❌ 避免过度嵌套
// DSL应该扁平化,过深的嵌套降低可读性
🔗 相关知识点
- #104 扩展函数:DSL中常用的技术
- #106 中缀函数:让DSL更自然
- #107 操作符重载:增强DSL表达能力
- #113 let作用域函数:apply在DSL中的应用
- #118 高阶函数与lambda表达式:DSL的基础
提示:DSL是Kotlin的强大特性,合理使用可以让代码更优雅、更易读。
更多推荐


所有评论(0)