(D)omain (S)pecific (L)anguage语言是一种用于描述特定域中事物的格式。一个非常基本的例子是购物清单:包含项目和可选项目计数的列表。对购物非常有用。但在这种情况下,DSL的目标是帮助开发人员。众所周知的例子是HTML和CSS,用于描述网页和样式的格式。对于我们人类来说,阅读和编写HTML和CSS比通过常规编程获得相同的结果更容易。但是您确实需要一个专用的DSL解析器。如果有一点编辑器支持,可以在您在DSL中编写时为您提供帮助,那就太好了。
在 Kotlin 中,您可以使用类型安全生成器来实现此目的。它们用于诸如格拉德DSL和设置Ktor服务器之类的事情。但是开发人员也可以使用它们为其特定域编写DSL。它们提供类型安全和编辑器支持(代码完成),因此使用它们大多是无痛的。
这很好,但是作为开发人员,您希望了解自己在做什么以及为什么有效。Kotlin DSL并不直观,它们依赖于一些先进的Kotlin概念。不幸的是,标准文档(https://kotlinlang.org/docs/reference/type-safe-builders.html)相当快地讨论了这些概念。
本教程旨在全面解释类型安全生成器背后的基本概念,然后说明如何使用它来创建此 DSL:
data class Person(val age: Int, val greeting: Greeting) data class Greeting(val specific: Map<String, String>, val default: String) fun main() { val person = buildPerson { age = 42 greeting { "Hello" to "father" "Hi" to "mother" default = "What's up" } } println(person) }
哪些输出:
Person(age=42, greeting=Greeting( specific={mother=Hi, father=Hello}, default=What's up))
在解释基本概念时,我们将采取初学者步骤,以确保您可以遵循。然后,我们将加快速度来解释如何制作我们的DSL。在本教程结束时,您将了解其工作原理和原因,以及如何自己编写必要的构建器。
提供了指向Kotlin游乐场代码的链接。自己玩这些概念真的很有帮助,所以如果你感到困惑,只需玩一下代码。
第 1 部分 – 基础知识
扩展函数
Kotlin DSL 是使用扩展函数构建的,因此让我们先来看看普通扩展函数的基础知识。有关详细信息,请参阅 https://kotlinlang.org/docs/reference/extensions.html。
data class Person ( var age: Int = 0 ) fun Person.defineAge() { // this: Person this.age = 42 } fun main() { val person = Person() person.defineAge() println(person) }
此代码输出:
Person(age=42)
尝试使用此代码:https://pl.kotl.in/91ERUDeaK
在扩展函数中,是 Person 对象。我们可以使用它来访问 person 对象的任何公共函数和属性。虽然我们似乎以某种方式在 Person 类中添加了功能,但我们不是。我们只能访问公共函数和属性,因为我们被交给的对象(在名称下),就像驻留在 Person 类之外的任何其他代码段一样。fun Person.setAge()
this
this
this
这一切都只是编译器提供的语法糖。编写相同功能的另一种方法是:
fun normalDefineAge(person: Person) { person.age = 42 } normalDefineAge(person)
扩展功能更易于阅读,因此这是它们的一大优势。另一个是,从某种意义上说,这是特殊的,因为它可以省略。所以我们也可以这样写:this
fun Person.defineAge() { // this: Person age = 42 }
将一个扩展函数传递给另一个函数
函数是 Kotlin 中的一等公民,扩展函数也是如此。因此,我们可以将它们作为参数传递给其他函数。对于扩展函数来说,那会是什么样子?
data class Person ( var age: Int = 0 ) fun Person.defineAge() { // this: Person this.age = 42 } fun buildPerson(init: Person.() -> Unit): Person { val person = Person() person.init() return person } fun main() { val person = buildPerson(Person::defineAge) println(person) }
在构建人中,您会看到扩展函数的函数类型为 。这是有道理的:它是一个扩展 Person 的函数,没有参数,也不返回任何内容。init 函数不能由自身调用,它必须在 Person 上调用。Person.() -> Unit
虽然它可能看起来有点奇怪,但它仍然只是能够将一个(扩展)函数传递给另一个函数的自然结果。结果非常棒:通过调用,我们可以更改 的内容,如在构建器函数外部所定义的那样。这是Kotlin DSL的基础。person.init()person
使用 lambda 作为扩展函数
上一节中的扩展函数可以通过将其编写为匿名函数来分配给值:fun Person.defineAge() {}
val defineAgeFunction: Person.() -> Unit = fun Person.() { this.age = 42 }
使用 Kotlin 类型推断,我们可以将其缩写为:
val defineAgeFunction = fun Person.() { this.age = 42 }
如果我们稍微重写它,我们得到lambda语法:
val defineAgeLambda: Person.() -> Unit = { this.age = 42 }
对我来说,最后一行看起来很奇怪:我认为类型推断是从右到左工作的。如:setAge 的类型派生自我们等号(我们的 lambda 函数)右侧的内容。如果这就是它的全部内容,我们将得到一个编译错误。右边的东西没有意义。
对于 lambdas,它从左到右工作:我们在等号的左侧指定我们需要的类型,这会影响其右侧写入的内容。借用鸭子打字的比喻,编译器告诉我们:你是一只鸭子,所以可以像一只鸭子一样随意走路和嘎嘎叫。
也许你可以看到我们要去哪里。我们可以将值传递给 ,但我们也可以内联 lambda 表达式,以避免有一个单独的定义AgeLambda。如果函数的最后一个参数是 lambda,我们可以将表达式移到括号后面。如果没有其他参数,我们可以完全省略括号。defineAgeLambda
buildPerson()
data class Person ( var age: Int = 0 ) val defineAgeLambda: Person.() -> Unit = { this.age = 1 } fun main() { // pass the extension function as a lambda value val person1 = buildPerson(defineAgeLambda) // write the argument as an inline lambda val person2 = buildPerson({ this.age = 2 }) // move the lambda expression outside the buildPerson parenthesis val person3 = buildPerson() { this.age = 3 } // omit the empty parenthesis val person4 = buildPerson { this.age = 4 } // omit `this` val person5 = buildPerson { age = 5 } println("$person1, $person2, $person3, $person4, $person5") } fun buildPerson(init: Person.() -> Unit): Person { val person = Person() person.init() return person }
哪些输出:
Person(age=1), Person(age=2), Person(age=3), Person(age=4), Person(age=5)
我们有它:DSL的第一部分。不再可见的是,通过使用编写为内联 lambda 的扩展函数来实际设置 Person 对象的年龄。age = 5
this
如果你理解了这一点,那么你就理解了DSL是如何工作的。我们可以使DSL更有用,其实现更干净,但基础知识保持不变。所以玩这个代码!
第 2 部分 – 制作我们的 DSL
现在我们已经掌握了基本概念,我们可以研究如何创建开头所示的完整DSL。
易变性
第 1 部分中的示例依赖于 .这通常不是我们想要的,但我们需要它才能设置其值。解决方案是拥有人员的构建器版本(带有)和人员的最终版本,如下所示:age
var
var age
val age
data class Person(val age: Int) fun main() { val person: Person = buildPerson { // this: PersonBuilder age = 42 } println(person) } class PersonBuilder { var age = 0 fun build(): Person { return Person(this.age) } } fun buildPerson(init: PersonBuilder.() -> Unit): Person { val builder = PersonBuilder() builder.init() return builder.build() }
嵌 套
虽然具有不可变的值是我们想要的,但构建器类使DSL代码更难阅读。我们的lambda不再扩展(我们知道和理解的类),但它扩展了隐藏在DSL代码中的中间类。Person
但是构建器类确实有另一个存在的理由。它们允许我们添加其他函数,我们需要这些函数来创建嵌套属性。让我们看一下向某人添加问候语的代码,现在只将自己限制为默认问候语。
data class Person(val age: Int, val greeting: Greeting) data class Greeting(val default: String) fun main() { val person1: Person = buildPerson { // this: PersonBuilder this.age = 1 this.greeting({ // this: GreetingBuilder this.default = "How are you" }) } val person2: Person = buildPerson { age = 2 greeting { default = "What's up" } } println("$person1, $person2") } class GreetingBuilder { var default = "" fun build(): Greeting { return Greeting(default) } } class PersonBuilder { var age = 0 private val greetingBuilder = GreetingBuilder() fun greeting(init: GreetingBuilder.() -> Unit) { greetingBuilder.init() } fun build(): Person { return Person(this.age, this.greetingBuilder.build()) } } fun buildPerson(init: PersonBuilder.() -> Unit): Person { val builder = PersonBuilder() builder.init() return builder.build() }
正在重用我们的基本 DSL 构建块 (扩展函数 lambda) 来创建嵌套的问候属性。PersonBuilder
扩展函数的有用技巧
为了使我们的DSL看起来不错,我们可以用另一种方式使用扩展函数。例如,我们可以将类的扩展函数声明为构建器类中的成员函数。这与仅创建 String 的扩展函数非常相似,但我们将范围限制为生成器类。String
class GreetingBuilder { var default = "" fun String.useAsDefault () { // this: String default = this } fun useFormal() { "How do you do?".useAsDefault() } fun build(): Greeting { return Greeting( default) } }
该函数不能在全局范围内使用,但必须在问候生成器的上下文中使用,就像在 中一样。useAsDefault()
fun useFormal()
但是,它仍然是问候生成器类的公共部分,因此它也可用于问候生成器类的扩展函数。这意味着我们可以在DSL lambda中使用它。例如:
data class Greeting(val default: String) fun main() { val greeting: Greeting = buildGreeting { // this: GreetingBuilder "What's up".useAsDefault() } println(greeting) } fun buildGreeting(init: GreetingBuilder.() -> Unit): Greeting { val builder = GreetingBuilder() builder.init() return builder.build() }
使用此构造,我们可以创建 DSL 的最后一部分。我们使用哈希图来存储特定的问候语,并使用字符串扩展函数使填充地图看起来不错。为了抛光,我们使用中缀符号来使它看起来更好。
data class Greeting(val specific: Map<String, String>, val default: String) fun main() { val greeting: Greeting = buildGreeting { // this: GreetingBuilder "Hello".to("father") // using String extension member function "Hi" to "mother" // also making use of infix notation default = "What's up" } println(greeting) } class GreetingBuilder { var default = "" private val specific = HashMap<String, String>() infix fun String.to (target: String) { specific[target] = this } fun build(): Greeting { return Greeting(specific, default) } } fun buildGreeting(init: GreetingBuilder.() -> Unit): Greeting { val builder = GreetingBuilder() builder.init() return builder.build() }
@DslMarker
看起来我们都完成了,我们实现了我们的目标。但是,这个难题还有一块。再考虑嵌套构建器的情况:
fun main() { val person: Person = buildPerson { // this: PersonBuilder age = 2 greeting { // this: GreetingBuilder default = "What's up" } } println(person) }
如果我们在 Kotlin Playground 中玩一会儿,我们可以看到代码完成告诉我们,我们可以访问年龄作为问候语的一部分。
事实上,这是真的。我们可以访问 中的 的属性。原因是我们仍然在人员构建器的范围内。因此,从编译器的角度来看,我们可以访问其属性是有道理的。PersonBuilder
GreetingBuilder
在一个作用域中有多个函数可能会感到奇怪,但这是使用扩展函数的结果。在将扩展函数编写为成员函数时,我们已经使用了它。this
class GreetingBuilder { var default = "" fun String.useAsDefault () { // this: String // access to GreetingBuilder.default via implicit this default = this // access to GreetingBuilder.default via explicit this this@GreetingBuilder.default = this } }
如您所见,我们还可以显式访问外部。this
幸运的是,Kotlin 中存在一种机制,用于告诉编译器阻止隐式可用:。有了它,我们定义了一个注释,用于将我们的代码标记为我们的DSL语言。this
@DslMarker
@DslMarker annotation class PersonDsl
现在,我们可以注释我们的人员生成器和 HelloBuilder 类,使其成为 的一部分。PersonDSL
data class Person(val age: Int, val greeting: Greeting) data class Greeting(val specific: Map<String, String>, val default: String) @DslMarker annotation class PersonDsl fun main() { val person = buildPerson { age = 42 greeting { "Hello" to "father" "Hi" to "mother" default = "What's up" // age = 41 // Uncommenting the line above will yield a compile error. this@buildPerson.age = 43 // But we can still be explicit } } println(person) } @PersonDsl class GreetingBuilder { var default = "" private val specific = HashMap<String, String>() infix fun String.to (target: String) { specific[target] = this } fun build(): Greeting { return Greeting(specific, default) } } @PersonDsl class PersonBuilder { var age = 0 private val greetingBuilder = GreetingBuilder() fun greeting(init: GreetingBuilder.() -> Unit) { greetingBuilder.init() } fun build(): Person { return Person(this.age, this.greetingBuilder.build()) } } fun buildPerson(init: PersonBuilder.() -> Unit): Person { val builder = PersonBuilder() builder.init() return builder.build() }
我们有了它:我们开始编写的DSL。我们正在防止我们在块内意外引用。@PersonDsl
age
greeting {}
您可能想知道为什么我们必须定义我们的DSL语言注释,以及为什么我们不能在我们的构建器类上打上标签。原因是通过这种方式,多个DSL语言可以共存。但是,不要让我举一个例子,;)PersonDsl
@DslMarker
结论
DSL语言是帮助您编写正确代码或配置的好工具。希望本指南有助于揭开它在引擎盖下的工作原理。这将帮助您在使用 DSL 时解决问题,也许它会激发您编写自己的 Kotlin DSL!