Kotlin DSL, 指用Kotlin写的Domain Specific Language. 本文经过解析官方的Kotlin DSL写html的例子, 来讲明Kotlin DSL是什么.html
首先是一些基础知识, 包括什么是DSL, 实现DSL利用了那些Kotlin的语法, 经常使用的情形和流行的库.java
对html实例的解析, 没有一冲上来就展现正确答案, 而是按照分析需求, 设计, 和实现细化的步骤来逐步让解决方案变得明朗清晰.android
DSL: Domain Specific Language. 专一于一个方面而特殊设计的语言.git
能够看作是封装了一套东西, 用于特定的功能, 优点是复用性和可读性的加强. -> 意思是提取了一套库吗?github
不是.数据库
DSL和简单的方法提取不一样, 有可能代码的形式或者语法变了, 更接近天然语言, 更容易让人看懂.安全
作一个DSL, 改变语法, 在Kotlin中主要依靠:bash
三个lambda语法:服务器
it
直接表示.()
外面. 若是lambda是惟一的参数, 能够省略小括号()
.扩展方法.网络
Gradle的build文件就是用DSL写的. 以前是Groovy DSL, 如今也有Kotlin DSL了.
还有Anko. 这个库包含了不少功能, UI组件, 网络, 后台任务, 数据库等.
和服务器端用的: Ktor
应用场景: Type-Safe Builders type-safe builders指类型安全, 静态类型的builders.
这种builders就比较适合建立Kotlin DSL, 用于构建复杂的层级结构数据, 用半陈述式的方式.
官方文档举的是html的例子. 后面就对这个例子进行一个梳理和解析.
首先明确一下咱们的目标.
作一个最简单的假设, 咱们期待的结果是在Kotlin代码中相似这样写:
html {
head { }
body { }
}
复制代码
就能输出这样的文本:
<html>
<head>
</head>
<body>
</body>
</html>
复制代码
仔细观察第一段Kotlin代码, html{}
应该是一个方法调用, 只不过这个方法只有一个lambda表达式做为参数, 因此省略了()
.
里面的head{}
和body{}
也是同理, 都是两个以lambda做为惟一参数的方法.
由于标签的层级关系, 能够理解为每一个标签都负责本身包含的内容, 父标签只负责按顺序显示子标签的内容.
因为<head>
和<body>
等标签只在<html>
标签中才有意义, 因此应该限制外部只能调用html{}
方法, head{}
和body{}
方法只有在html{}
的方法体中才能调用.
由于标签看起来都是相似的, 为了代码复用, 首先设计一个抽象的标签类Tag
, 包含:
文字比较特殊, 它不带标签符号<>
, 就输出本身. 因此它的渲染方法就是输出文字自己.
能够提取出一个更加基类的接口Element
, 只包含渲染方法. 这个接口的子类是Tag
和TextElement
.
有文字的标签, 如<title>
, 它的输出结果:
<title>
HTML encoding with Kotlin
</title>
复制代码
文字元素是做为标签的一个子标签的. 这里的实现不容易本身想到, 直接看后面的实现部分揭晓答案吧.
有了前面的心路历程, 再来看实现就能容易一些.
首先是最基本的接口, 只包含了渲染方法:
interface Element {
fun render(builder: StringBuilder, indent: String)
}
复制代码
它的直接子类标签类:
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()
}
}
复制代码
完成了自身标签名和属性的渲染, 接着遍历子标签渲染其内容. 注意这里为全部子标签加上了一层缩进.
initTag()
这个方法是protected
的, 供子类调用, 为本身加上子标签.
带文字的标签有个抽象的基类:
abstract class TagWithText(name: String) : Tag(name) {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
复制代码
这是一个对+
运算符的重载, 这个扩展方法把字符串包装成TextElement
类对象, 而后加到当前标签的子标签中去.
TextElement
作的事情就是渲染本身:
class TextElement(val text: String) : Element {
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent$text\n")
}
}
复制代码
因此, 当咱们调用:
html {
head {
title { +"HTML encoding with Kotlin" }
}
}
复制代码
获得结果:
<html>
<head>
<title>
HTML encoding with Kotlin
</title>
</html>
复制代码
其中用到的Title
类定义:
class Title : TagWithText("title")
复制代码
经过'+'运算符的操做, 字符串: "HTML encoding with Kotlin"被包装成了TextElement
, 他是title标签的child.
对外的公开方法只有这一个:
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
复制代码
init
参数是一个函数, 它的类型是HTML.() -> Unit
. 这是一个带接收器的函数类型, 也就是说, 须要一个HTML
类型的实例来调用这个函数.
这个方法实例化了一个HTML
类对象, 在实例上调用传入的lambda参数, 而后返回该对象.
调用此lambda的实例会被做为this
传入函数体内(this
能够省略), 咱们在函数体内就能够调用HTML
类的成员方法了.
这样保证了外部的访问入口, 只有:
html {
}
复制代码
经过成员函数建立内部标签.
HTML类以下:
class HTML : TagWithText("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
复制代码
能够看出html
内部能够经过调用head
和body
方法建立子标签, 也能够用+
来添加字符串.
这两个方法原本能够是这样:
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
}
复制代码
因为形式相似, 因此作了泛型抽象, 被提取到了基类Tag
中, 做为更加通用的方法:
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
复制代码
作的事情: 建立对象, 在其之上调用init lambda, 添加到子标签列表, 而后返回.
其余标签类的实现与之相似, 不做过多解释.
以上都写完了以后, 感受大功告成, 但其实还有一个隐患.
咱们竟然能够这样写:
html {
head {
title { +"HTML encoding with Kotlin" }
head { +"haha" }
}
}
复制代码
在head方法的lambda块中, html块的receiver仍然是可见的, 因此还能够调用head
方法. 显式地调用是这样的:
this@html.head { +"haha" }
复制代码
可是这里this@html.
是能够省略的.
这段代码输出的是:
<html>
<head>
haha
</head>
<head>
<title>
HTML encoding with Kotlin
</title>
</head>
</html>
复制代码
最内层的haha反却是最早被加到html对象的孩子列表里.
这种穿透性太混乱了, 容易致使错误, 咱们能不能限制每一个大括号里只有当前的对象成员是可访问的呢? -> 能够.
为了解决这种问题, Kotlin 1.1推出了管理receiver scope的机制, 解决方法是使用@DslMarker
.
html的例子, 定义注解类:
@DslMarker
annotation class HtmlTagMarker
复制代码
这种被@DslMarker
修饰的注解类叫作DSL marker
.
而后咱们只须要在基类上标注:
@HtmlTagMarker
abstract class Tag(val name: String)
复制代码
全部的子类都会被认为也标记了这个marker.
加上注解以后隐式访问会编译报错:
html {
head {
head { } // error: a member of outer receiver
}
// ...
}
复制代码
可是显式仍是能够的:
html {
head {
this@html.head { } // possible
}
// ...
}
复制代码
只有最近的receiver对象能够隐式访问.
本文经过实例, 来逐步解析如何用Kotlin代码, 用半陈述式的方式写html结构, 从而看起来更加直观. 这种就叫作DSL.
Kotlin DSL经过精心的定义, 主要的目的是为了让使用者更加方便, 代码更加清晰直观.
More resources: