# 类型安全的构建器
[TOC]
通过使用命名得当的函数作为构建器,结合[带有接收者的函数字面值](http://www.kotlincn.net/docs/reference/lambdas.html#%E5%B8%A6%E6%9C%89%E6%8E%A5%E6%94%B6%E8%80%85%E7%9A%84%E5%87%BD%E6%95%B0%E5%AD%97%E9%9D%A2%E5%80%BC),可以在 Kotlin 中创建类型安全、静态类型的构建器。
类型安全的构建器可以创建基于 Kotlin 的适用于采用半声明方式构建复杂层次数据结构领域专用语言(DSL)。以下是构建器的一些示例应用场景:
* 使用 Kotlin 代码生成标记语言,例如 [HTML](https://github.com/Kotlin/kotlinx.html) 或 XML;
* 以编程方式布局 UI 组件:[Anko](https://github.com/Kotlin/anko/wiki/Anko-Layouts);
* 为 Web 服务器配置路由:[Ktor](https://ktor.kotlincn.net/features/routing.html#routing-tree)。
## 一个类型安全的构建器示例
考虑下面的代码:
```kotlin
import com.example.html.* // 参见下文声明
fun result() =
html {
head {
title {+"XML encoding with Kotlin"}
}
body {
h1 {+"XML encoding with Kotlin"}
p {+"this format can be used as an alternative markup to XML"}
// 一个具有属性和文本内容的元素
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"}
// 以下代码生成的内容
p {
for (arg in args)
+arg
}
}
}
```
这是完全合法的 Kotlin 代码。你可以[在这里](https://play.kotlinlang.org/byExample/09_Kotlin_JS/06_HtmlBuilder)在线运行上文代码(修改它并在浏览器中运行)。
## 实现原理
让我们来看看 Kotlin 中实现类型安全构建器的机制。首先,我们需要定义我们想要构建的模型,在本例中我们需要建模 HTML 标签。用一些类就可以轻易完成。例如,`HTML` 是一个描述 `<html>` 标签的类,也就是说它定义了像 `<head>` 和 `<body>` 这样的子标签。(参见[下文](http://www.kotlincn.net/docs/reference/type-safe-builders.html#comexamplehtml-%E5%8C%85%E7%9A%84%E5%AE%8C%E6%95%B4%E5%AE%9A%E4%B9%89)它的声明。)
现在,让我们回想下为什么我们可以在代码中这样写:
```kotlin
html {
// ……
}
```
`html` 实际上是一个函数调用,它接受一个 [lambda 表达式](http://www.kotlincn.net/docs/reference/lambdas.html) 作为参数。该函数定义如下:
```kotlin
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
```
这个函数接受一个名为 `init` 的参数,该参数本身就是一个函数。该函数的类型是 `HTML.() -> Unit`,它是一个 _带接收者的函数类型_ 。这意味着我们需要向函数传递一个 HTML 类型的实例( _接收者_ ),并且我们可以在函数内部调用该实例的成员。该接收者可以通过 *this*关键字访问:
```kotlin
html {
this.head { …… }
this.body { …… }
}
```
(`head` 和 `body` 是 `HTML` 的成员函数。)
现在,像往常一样,*this* 可以省略掉了,我们得到的东西看起来已经非常像一个构建器了:
```kotlin
html {
head { …… }
body { …… }
}
```
那么,这个调用做什么? 让我们看看上面定义的 `html` 函数的主体。它创建了一个 `HTML` 的新实例,然后通过调用作为参数传入的函数来初始化它(在我们的示例中,归结为在HTML实例上调用 `head` 和 `body`),然后返回此实例。这正是构建器所应做的。`HTML` 类中的 `head` 和 `body` 函数的定义与 `html` 类似。唯一的区别是,它们将构建的实例添加到包含 `HTML` 实例的 `children` 集合中:
```kotlin
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`:
```kotlin
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
```
所以,现在我们的函数很简单:
```kotlin
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
```
并且可以使用它们来构建 `<head>` 和 `<body>` 标签。
这里要讨论的另一件事是如何向标签体中添加文本。在上例中我们这样写到:
```kotlin
html {
head {
title {+"XML encoding with Kotlin"}
}
// ……
}
```
所以基本上,我们只是把一个字符串放进一个标签体内部,但在它前面有一个小的 `+`,所以它是一个函数调用,调用一个前缀 `unaryPlus()` 操作。该操作实际上是由一个扩展函数 `unaryPlus()` 定义的,该函数是 `TagWithText` 抽象类(`Title` 的父类)的成员:
```kotlin
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
```
所以,在这里前缀 `+` 所做的事情是把一个字符串包装到一个 `TextElement` 实例中,并将其添加到 `children` 集合中,以使其成为标签树的一个适当的部分。
所有这些都在上面构建器示例顶部导入的包 `com.example.html` 中定义。在最后一节中,你可以阅读这个包的完整定义。
## 作用域控制:@DslMarker(自 1.1 起)
使用 DSL 时,可能会遇到上下文中可以调用太多函数的问题。我们可以调用 lambda 表达式内部每个可用的隐式接收者的方法,因此得到一个不一致的结果,就像在另一个 `head` 内部的 `head` 标记那样:
```kotlin
html {
head {
head {} // 应该禁止
}
// ……
}
```
在这个例子中,必须只有最近层的隐式接收者 `this@head` 的成员可用;`head()` 是外部接收者 `this@html` 的成员,所以调用它一定是非法的。
为了解决这个问题,在 Kotlin 1.1 中引入了一种控制接收者作用域的特殊机制。
为了使编译器开始控制标记,我们只是必须用相同的标记注解来标注在 DSL 中使用的所有接收者的类型。例如,对于 HTML 构建器,我们声明一个注解 `@HTMLTagMarker`:
```kotlin
@DslMarker
annotation class HtmlTagMarker
```
如果一个注解类使用 `@DslMarker` 注解标注,那么该注解类称为 DSL 标记。
在我们的 DSL 中,所有标签类都扩展了相同的超类 `Tag`。只需使用 `@HtmlTagMarker` 来标注超类就足够了,之后,Kotlin 编译器会将所有继承的类视为已标注:
```kotlin
@HtmlTagMarker
abstract class Tag(val name: String) { …… }
```
我们不必用 `@HtmlTagMarker` 标注 `HTML` 或 `Head` 类,因为它们的超类已标注过:
```
class HTML() : Tag("html") { …… }
class Head() : Tag("head") { …… }
```
在添加了这个注解之后,Kotlin 编译器就知道哪些隐式接收者是同一个 DSL 的一部分,并且只允许调用最近层的接收者的成员:
```kotlin
html {
head {
head { } // 错误:外部接收者的成员
}
// ……
}
```
请注意,仍然可以调用外部接收者的成员,但是要做到这一点,你必须明确指定这个接收者:
```kotlin
html {
head {
this@html.head { } // 可能
}
// ……
}
```
## `com.example.html` 包的完整定义
这就是 `com.example.html` 包的定义(只有上面例子中使用的元素)。它构建一个 HTML 树。代码中大量使用了[扩展函数](http://www.kotlincn.net/docs/reference/extensions.html)和[带有接收者的 lambda 表达式](http://www.kotlincn.net/docs/reference/lambdas.html#%E5%B8%A6%E6%9C%89%E6%8E%A5%E6%94%B6%E8%80%85%E7%9A%84%E5%87%BD%E6%95%B0%E5%AD%97%E9%9D%A2%E5%80%BC)。
请注意,`@DslMarker` 注解在 Kotlin 1.1 起才可用。
```kotlin
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
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
}
```
- 前言
- Kotlin简介
- IntelliJ IDEA技巧总结
- idea设置类注释和方法注释模板
- 像Android Studion一样创建工程
- Gradle
- Gradle入门
- Gradle进阶
- 使用Gradle创建一个Kotlin工程
- 环境搭建
- Androidstudio平台搭建
- Eclipse的Kotlin环境配置
- 使用IntelliJ IDEA
- Kotlin学习路线
- Kotlin官方中文版文档教程
- 概述
- kotlin用于服务器端开发
- kotlin用于Android开发
- kotlin用于JavaScript开发
- kotlin用于原生开发
- Kotlin 用于数据科学
- 协程
- 多平台
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 开始
- 基本语法
- 习惯用法
- 编码规范
- 基础
- 基本类型
- 包与导入
- 控制流
- 返回与跳转
- 类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类
- 枚举类
- 对象
- 类型别名
- 内嵌类
- 委托
- 委托属性
- 函数与Lambda表达式
- 函数
- Lambda表达式
- 内联函数
- 集合
- 集合概述
- 构造集合
- 迭代器
- 区间与数列
- 序列
- 操作概述
- 转换
- 过滤
- 加减操作符
- 分组
- 取集合的一部分
- 取单个元素
- 排序
- 聚合操作
- 集合写操作
- List相关操作
- Set相关操作
- Map相关操作
- 多平台程序设计
- 平台相关声明
- 以Gradle创建
- 更多语言结构
- 解构声明
- 类型检测与转换
- This表达式
- 相等性
- 操作符重载
- 空安全
- 异常
- 注解
- 反射
- 作用域函数
- 类型安全的构造器
- Opt-in Requirements
- 核心库
- 标准库
- kotlin.test
- 参考
- 关键字与操作符
- 语法
- 编码风格约定
- Java互操作
- Kotlin中调用Java
- Java中调用Kotlin
- JavaScript
- 动态类型
- kotlin中调用JavaScript
- JavaScript中调用kotlin
- JavaScript模块
- JavaScript反射
- JavaScript DCE
- 原生
- 并发
- 不可变性
- kotlin库
- 平台库
- 与C语言互操作
- 与Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 调试
- FAQ
- 协程
- 协程指南
- 基础
- 取消与超时
- 组合挂起函数
- 协程上下文与调度器
- 异步流
- 通道
- 异常处理与监督
- 共享的可变状态与并发
- Select表达式(实验性)
- 工具
- 编写kotlin代码文档
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin与OSGI
- 编译器插件
- 编码规范
- 演进
- kotlin语言演进
- 不同组件的稳定性
- kotlin1.3的兼容性指南
- 常见问题
- FAQ
- 与Java比较
- 与Scala比较(官方已删除)
- Google开发者官网简介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin样品
- Kotlin零基础到进阶
- 第一阶段兴趣入门
- kotlin简介和学习方法
- 数据类型和类型系统
- 入门
- 分类
- val和var
- 二进制基础
- 基础
- 基本语法
- 包
- 示例
- 编码规范
- 代码注释
- 异常
- 根类型“Any”
- Any? 可空类型
- 可空性的实现原理
- kotlin.Unit类型
- kotlin.Nothing类型
- 基本数据类型
- 数值类型
- 布尔类型
- 字符型
- 位运算符
- 变量和常量
- 语法和运算符
- 关键字
- 硬关键字
- 软关键字
- 修饰符关键字
- 特殊标识符
- 操作符和特殊符号
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- this关键字
- super关键字
- 操作符重载
- 一元操作符
- 二元操作符
- 字符串
- 字符串介绍和属性
- 字符串常见方法操作
- 字符串模板
- 数组
- 数组介绍创建及遍历
- 数组常见方法和属性
- 数组变化以及下标越界问题
- 原生数组类型
- 区间
- 正向区间
- 逆向区间
- 步长
- 类型检测与类型转换
- is、!is、as、as-运算符
- 空安全
- 可空类型变量
- 安全调用符
- 非空断言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函数
- 函数式编程概述
- OOP和FOP
- 函数式编程基本特性
- 组合与范畴
- 在Kotlin中使用函数式编程
- 函数入门
- 函数作用域
- 函数加强
- 命名参数
- 默认参数
- 可变参数
- 表达式函数体
- 顶层、嵌套、中缀函数
- 尾递归函数优化
- 函数重载
- 控制流
- if表达式
- when表达式
- for循环
- while循环
- 循环中的 Break 与 continue
- return返回
- 标签处返回
- 集合
- list集合
- list集合介绍和操作
- list常见方法和属性
- list集合变化和下标越界
- set集合
- set集合介绍和常见操作
- set集合常见方法和属性
- set集合变换和下标越界
- map集合
- map集合介绍和常见操作
- map集合常见方法和属性
- map集合变换
- 集合的函数式API
- map函数
- filter函数
- “ all ”“ any ”“ count ”和“ find ”:对集合应用判断式
- 别样的求和方式:sumBy、sum、fold、reduce
- 根据人的性别进行分组:groupBy
- 扁平化——处理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 区间、数组、集合之间转换
- 面向对象
- 面向对象-封装
- 类的创建及属性方法访问
- 类属性和字段
- 构造器
- 嵌套类(内部类)
- 枚举类
- 枚举类遍历&枚举常量常用属性
- 数据类
- 密封类
- 印章类(密封类)
- 面向对象-继承
- 类的继承
- 面向对象-多态
- 抽象类
- 接口
- 接口和抽象类的区别
- 面向对象-深入
- 扩展
- 扩展:为别的类添加方法、属性
- Android中的扩展应用
- 优化Snackbar
- 用扩展函数封装Utils
- 解决烦人的findViewById
- 扩展不是万能的
- 调度方式对扩展函数的影响
- 被滥用的扩展函数
- 委托
- 委托类
- 委托属性
- Kotlin5大内置委托
- Kotlin-Object关键字
- 单例模式
- 匿名类对象
- 伴生对象
- 作用域函数
- let函数
- run函数
- with函数
- apply函数
- also函数
- 标准库函数
- takeIf 与 takeUnless
- 第二阶段重点深入
- Lambda编程
- Lambda成员引用高阶函数
- 高阶函数
- 内联函数
- 泛型
- 泛型的分类
- 泛型约束
- 子类和子类型
- 协变与逆变
- 泛型擦除与实化类型
- 泛型类型参数
- 泛型的背后:类型擦除
- Java为什么无法声明一个泛型数组
- 向后兼容的罪
- 类型擦除的矛盾
- 使用内联函数获取泛型
- 打破泛型不变
- 一个支持协变的List
- 一个支持逆变的Comparator
- 协变和逆变
- 第三阶段难点突破
- 注解和反射
- 声明并应用注解
- DSL
- 协程
- 协程简介
- 协程的基本操作
- 协程取消
- 管道
- 慕课霍丙乾协程笔记
- Kotlin与Java互操作
- 在Kotlin中调用Java
- 在Java中调用Kotlin
- Kotlin与Java中的操作对比
- 第四阶段专题练习
- 朱凯Kotlin知识点总结
- Kotlin 基础
- Kotlin 的变量、函数和类型
- Kotlin 里那些「不是那么写的」
- Kotlin 里那些「更方便的」
- Kotlin 进阶
- Kotlin 的泛型
- Kotlin 的高阶函数、匿名函数和 Lambda 表达式
- Kotlin协程
- 初识
- 进阶
- 深入
- Kotlin 扩展
- 会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions / Properties)
- Kotlin实战-开发Android