5.7.3.1 类型安全的构建器
原文链接: https://kotlinlang.org/docs/type-safe-builders.html
5.7.3.1 类型安全的构建器
通过把命名良好的函数用作构建器,并结合带接收者的函数字面量,就可以在 Kotlin 中创建类型安全的静态类型构建器。
类型安全的构建器允许创建基于 Kotlin 的领域特定语言(DSL),适合以半声明式的方式构建复杂的层级数据结构。构建器的典型用例包括:
- 用 Kotlin 代码生成标记语言,例如 HTML 或 XML
- 为 Web 服务器配置路由:Ktor
请看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
| package html
fun main() {
val result = html {
head {
title { +"HTML encoding with Kotlin" }
}
body {
h1 { +"HTML encoding with Kotlin" }
p {
+"this format can be used as an"
+"alternative markup to HTML"
}
// 一个带属性和文本内容的元素
a(href = "http://kotlinlang.org") { +"Kotlin" }
// 混合内容
p {
+"This is some"
b { +"mixed" }
+"text. For more see the"
a(href = "http://kotlinlang.org") {
+"Kotlin"
}
+"project"
}
p {
+"some text"
ul {
for (i in 1..5)
li { +"${i}*2 = ${i*2}" }
}
}
}
}
println(result)
}
interface Element {
fun render(builder: StringBuilder, indent: String)
}
class TextElement(val text: String) : Element {
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent$text\n")
}
}
@DslMarker
annotation class HtmlTagMarker
@HtmlTagMarker
abstract class Tag(val name: String) : Element {
val children = arrayListOf<Element>()
val attributes = hashMapOf<String, String>()
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent<$name${renderAttributes()}>\n")
for (c in children) {
c.render(builder, indent + " ")
}
builder.append("$indent</$name>\n")
}
private fun renderAttributes(): String {
val builder = StringBuilder()
for ((attr, value) in attributes) {
builder.append(" $attr=\"$value\"")
}
return builder.toString()
}
override fun toString(): String {
val builder = StringBuilder()
render(builder, "")
return builder.toString()
}
}
abstract class TagWithText(name: String) : Tag(name) {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
class HTML() : TagWithText("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
class Head() : TagWithText("head") {
fun title(init: Title.() -> Unit) = initTag(Title(), init)
}
class Title() : TagWithText("title")
abstract class BodyTag(name: String) : TagWithText(name) {
fun b(init: B.() -> Unit) = initTag(B(), init)
fun p(init: P.() -> Unit) = initTag(P(), init)
fun h1(init: H1.() -> Unit) = initTag(H1(), init)
fun ul(init: UL.() -> Unit) = initTag(UL(), init)
fun a(href: String, init: A.() -> Unit) {
val a = initTag(A(), init)
a.href = href
}
}
class Body() : BodyTag("body")
class UL() : BodyTag("ul") {
fun li(init: LI.() -> Unit) = initTag(LI(), init)
}
class B() : BodyTag("b")
class LI() : BodyTag("li")
class P() : BodyTag("p")
class H1() : BodyTag("h1")
class A : BodyTag("a") {
var href: String
get() = attributes["href"]!!
set(value) {
attributes["href"] = value
}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
|
<html>
<head>
<title>
HTML encoding with Kotlin
</title>
</head>
<body>
<h1>
HTML encoding with Kotlin
</h1>
<p>
this format can be used as an
alternative markup to HTML
</p>
<a href="http://kotlinlang.org">
Kotlin
</a>
<p>
This is some
<b>
mixed
</b>
text. For more see the
<a href="http://kotlinlang.org">
Kotlin
</a>
project
</p>
<p>
some text
<ul>
<li>
1*2 = 2
</li>
<li>
2*2 = 4
</li>
<li>
3*2 = 6
</li>
<li>
4*2 = 8
</li>
<li>
5*2 = 10
</li>
</ul>
</p>
</body>
</html>
示例输出
工作原理
假设你要在 Kotlin 中实现一个类型安全的构建器。首先,定义你想要构建的模型。在本例中,你需要为 HTML 标签建模。用一组类就能轻松做到。例如,HTML 是一个描述 <html> 标签的类,它定义了 <head> 和 <body> 这样的子元素。(它的声明见下文。)
现在回想一下,为什么可以在代码中这样写:
html 实际上是一个接收 lambda 表达式作为实参的函数调用。这个函数的定义如下:
1
2
3
4
5
| fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
|
这个函数接收一个名为 init 的参数,它本身就是一个函数。该函数的类型是 HTML.() -> Unit,即带接收者的函数类型。这意味着你需要向该函数传入一个 HTML 类型的实例(即接收者),并且可以在函数内部调用该实例的成员。
接收者可以通过 this 关键字访问:
1
2
3
4
| html {
this.head { ... }
this.body { ... }
}
|
(head 和 body 是 HTML 的成员函数。)
和往常一样,this 可以省略,于是得到的东西看起来已经非常像一个构建器了:
1
2
3
4
| html {
head { ... }
body { ... }
}
|
那么这个调用做了什么?来看看上面定义的 html 函数的函数体。它创建一个新的 HTML 实例,然后通过调用作为实参传入的函数来初始化它(在本例中,就是在这个 HTML 实例上调用 head 和 body),最后返回这个实例。这正是构建器应该做的事。
HTML 类中的 head 和 body 函数定义方式与 html 类似。唯一的区别是它们会把构建出的实例添加到外层 HTML 实例的 children 集合中:
1
2
3
4
5
6
7
8
9
10
11
12
13
| fun head(init: Head.() -> Unit): Head {
val head = Head()
head.init()
children.add(head)
return head
}
fun body(init: Body.() -> Unit): Body {
val body = Body()
body.init()
children.add(body)
return body
}
|
实际上这两个函数做的事情完全一样,所以你可以写一个通用版本 initTag:
1
2
3
4
5
| protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
|
于是你的函数就变得非常简单:
1
2
3
| fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
|
然后你就可以用它们来构建 <head> 和 <body> 标签了。
这里还要讨论的另一件事是如何向标签体添加文本。在上面的例子中,你会写出类似这样的代码:
1
2
3
4
5
6
| html {
head {
title {+"XML encoding with Kotlin"}
}
// ...
}
|
所以基本上就是把字符串放进标签体中,但字符串前面有一个小小的 +,因此它其实是一次函数调用,会调用前缀 unaryPlus() 运算。该运算实际上由扩展函数 unaryPlus() 定义,它是 TagWithText 抽象类(Title 的父类)的成员:
1
2
3
| operator fun String.unaryPlus() {
children.add(TextElement(this))
}
|
因此,这里的 + 前缀所做的是把字符串包装成 TextElement 实例并添加到 children 集合中,使它成为标签树中恰当的一部分。
所有这些都定义在 com.example.html 包中,并在上面构建器示例的顶部导入。在最后一节中,你可以通读这个包的完整定义。
作用域控制:@DslMarker
使用 DSL 时,人们可能遇到过这样的问题:在上下文中可以调用的函数太多了。你可以在 lambda 内部调用每个可用的隐式接收者的方法,从而得到不一致的结果,例如在 head 内部再写一个 head:
1
2
3
4
5
6
| html {
head {
head {} // 应当被禁止
}
// ...
}
|
在这个例子中,应当只有最近的隐式接收者 this@head 的成员可用;head() 是外层接收者 this@html 的成员,因此调用它必须是非法的。
为了解决这个问题,有一种控制接收者作用域的特殊机制。
要让编译器开始控制作用域,只需用同一个标记注解为 DSL 中使用的所有接收者类型添加注解。例如,对于 HTML 构建器,你声明一个注解 @HtmlTagMarker:
1
2
3
| @DslMarker
@Target(AnnotationTarget.CLASS)
annotation class HtmlTagMarker
|
如果一个注解类被 @DslMarker 注解标注,它就称为 DSL 标记。
@Target 注解限制了 @HtmlTagMarker 可以应用的位置。DSL 标记只有在应用于以下位置时才会影响作用域控制:
- 类型声明(
CLASS):用作 DSL 接收者的类或接口。 - 类型使用(
TYPE):函数类型签名中的接收者类型。 - 类型别名(
TYPEALIAS):展开为 DSL 接收者类型的类型别名。
把 DSL 标记应用到其他目标(例如函数或属性)对作用域控制没有任何影响。
注意: 关于 DSL 标记的工作原理的更多细节,请参阅相应的 KEEP 文档。
在我们的 DSL 中,所有标签类都继承同一个超类 Tag。只需要给这个超类加上 @HtmlTagMarker 注解,此后 Kotlin 编译器就会把所有继承来的类都视为已注解:
1
2
| @HtmlTagMarker
abstract class Tag(val name: String) { ... }
|
你不需要给 HTML 或 Head 类加 @HtmlTagMarker 注解,因为它们的超类已经加了:
1
2
3
| class HTML() : Tag("html") { ... }
class Head() : Tag("head") { ... }
|
添加这个注解之后,Kotlin 编译器就知道哪些隐式接收者属于同一个 DSL,并只允许调用最近接收者的成员:
1
2
3
4
5
6
| html {
head {
head { } // 错误:这是外层接收者的成员
}
// ...
}
|
注意,仍然可以调用外层接收者的成员,但必须显式指定该接收者:
1
2
3
4
5
6
| html {
head {
this@html.head { } // 可以
}
// ...
}
|
你也可以把 @DslMarker 注解直接应用于函数类型。这需要在注解目标中加上 AnnotationTarget.TYPE:
1
2
3
| @DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class HtmlTagMarker
|
这样一来,@DslMarker 注解就可以应用于函数类型,最常见的是用于带接收者的 lambda。例如:
1
2
3
4
5
| fun html(init: @HtmlTagMarker HTML.() -> Unit): HTML { ... }
fun HTML.head(init: @HtmlTagMarker Head.() -> Unit): Head { ... }
fun Head.title(init: @HtmlTagMarker Title.() -> Unit): Title { ... }
|
调用这些函数时,@DslMarker 注解会限制对被标记 lambda 主体中外层接收者的访问,除非你显式指定它们:
1
2
3
4
5
6
7
| html {
head {
title {
// 在这里访问 title、head 或外层接收者的其他函数会受到限制。
}
}
}
|
在 lambda 内部只能访问最近接收者的成员和扩展,从而避免嵌套作用域之间的意外交互。
当隐式接收者的成员与来自上下文参数的声明在同一作用域中同名时,编译器会发出警告,因为隐式接收者被上下文参数遮蔽了。要解决这个问题,可以用 this 限定符显式调用接收者,或用 contextOf<T>() 调用上下文声明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| interface HtmlTag {
fun setAttribute(name: String, value: String)
}
// 声明一个同名的顶层函数,
// 它通过上下文参数可用
context(tag: HtmlTag)
fun setAttribute(name: String, value: String) { tag.setAttribute(name, value) }
fun test(head: HtmlTag, extraInfo: HtmlTag) {
with(head) {
// 在内层作用域中引入同类型的上下文值
context(extraInfo) {
// 发出警告:
// 使用的是被上下文参数遮蔽的隐式接收者
setAttribute("user", "1234")
// 显式调用接收者的成员
this.setAttribute("user", "1234")
// 显式调用上下文声明
contextOf<HtmlTag>().setAttribute("user", "1234")
}
}
}
|
下面是 com.example.html 包的定义方式(只包含上面示例中用到的元素)。它构建了一棵 HTML 树,并大量使用了扩展函数和带接收者的 lambda。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
| package com.example.html
interface Element {
fun render(builder: StringBuilder, indent: String)
}
class TextElement(val text: String) : Element {
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent$text\n")
}
}
@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class HtmlTagMarker
@HtmlTagMarker
abstract class Tag(val name: String) : Element {
val children = arrayListOf<Element>()
val attributes = hashMapOf<String, String>()
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent<$name${renderAttributes()}>\n")
for (c in children) {
c.render(builder, indent + " ")
}
builder.append("$indent</$name>\n")
}
private fun renderAttributes(): String {
val builder = StringBuilder()
for ((attr, value) in attributes) {
builder.append(" $attr=\"$value\"")
}
return builder.toString()
}
override fun toString(): String {
val builder = StringBuilder()
render(builder, "")
return builder.toString()
}
}
abstract class TagWithText(name: String) : Tag(name) {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
class HTML : TagWithText("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
class Head : TagWithText("head") {
fun title(init: Title.() -> Unit) = initTag(Title(), init)
}
class Title : TagWithText("title")
abstract class BodyTag(name: String) : TagWithText(name) {
fun b(init: B.() -> Unit) = initTag(B(), init)
fun p(init: P.() -> Unit) = initTag(P(), init)
fun h1(init: H1.() -> Unit) = initTag(H1(), init)
fun a(href: String, init: A.() -> Unit) {
val a = initTag(A(), init)
a.href = href
}
}
class Body : BodyTag("body")
class B : BodyTag("b")
class P : BodyTag("p")
class H1 : BodyTag("h1")
class A : BodyTag("a") {
var href: String
get() = attributes["href"]!!
set(value) {
attributes["href"] = value
}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
|