工作原理
让我们来讨论一下Kotlin中类型安全Builder的实现机制。首先我们需要定义想创建的模块,在这种情况下,我们需要类似HTML的标签。这可以用一连串的类来实现。比如,HTML是一个描述<html>标签的类,该标签定义了一些子元素(比如<head>、<body>)。
html {
// ...
}
html是一个以lambda表达式作为参数的函数调用。这个函数的定义如下:
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
这个函数使用一个叫init的参数,其本身就是一个函数。函数的类型就是HTML.() -> Unit,这是一个带接收器的函数类型。这意味着我们需要传递一个HTML的实例给这个函数,并且我们可以在函数内部访问实例成员。接收器可以使用this关键字进行访问。
html {
this.head { /* ... */ }
this.body { /* ... */ }
}
head和body是HTML的成员函数。
this是可以省略的,通常,我们的书写方式更像一个Builder样式:
html {
head { /* ... */ }
body { /* ... */ }
}
那么,这个函数调用做了什么事情呢?我们继续查看上述html函数的定义。它创建了一个HTML的实例,然后调用作为参数传递过来的函数(init参数)进行初始化(在我们的示例中,这归结于调用HTML实例的head和body),然后返回这个实例,这就是一个Builder模式所做的操作。
在HTML类中head和body函数的定义与html函数很像。唯一的区别就是它们创建子节点集合到HTML实例中:
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:
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
因此,到目前为止,我们的函数很简洁:
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
因此我们可以使用它们来创建<head>和<body>标签。
另一个需要讨论的事情就是添加文本到标签主体。
html {
head {
title {+"XML encoding with Kotlin"}
}
// ...
}
我们仅仅是在标签主体添加了一个字符串,但是在内容前面有个+,因此这是一个函数调用,实际调用的是unaryPlus()操作。这个操作被定义在unaryPlus()扩展函数中,是TagWithText抽象类的一个成员(Title的父类)。
fun String.unaryPlus() {
children.add(TextElement(this))
}
因此这里的+将字符串包装到了一个TextElement的实例中,并且将其添加到了children集合中。
所有的这一切都在com.example.html包中定义了。在最后一个章节你可以查看所有的包定义细节。