通过使用命名得当的函数作为构建器,结合带有接收者的函数字面值,可以在 Kotlin 中创建类型安全、静态类型 的构建器

  类型安全的构建器可以创建基于 Kotlin 的适用于采用半声明方式构建复杂层次数据结构领域专用语言(DSL)。 以下是构建器的一些示例应用场景:

    — 使用 Kotlin 代码生成标记语言,例如 HTML 或 XML;

    — 以编程方式布局UI组件:Anko;

    — 为Web服务器配置路由:Ktor。

一个类型安全的构建器示例

  考虑下面的代码

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 代码。你可以在这里在线运行上文代码(修改它并在浏览器中运行)

实现原理

  让我们来看看 Kotlin 中实现类型安全构建器的机制。首先,我们需要定义我们想要构建的模型,在本例中我们 需要建模 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 可以省略掉了,我们得到的东西看起来已经非常像一个构建器了

html {
head { ...... }
body { ...... }
}

  那么,这个调用做什么?让我们看看上面定义的 html 函数的主体。它创建了一个 HTML 的新实例,然后通过 调用作为参数传入的函数来初始化它(在我们的示例中,归结为在HTML实例上调用 head 和 body),然后返 回此实例。这正是构建器所应做的。

  HTML 类中的 head 和 body 函数的定义与 html 类似。唯一的区别是,它们将构建的实例添加到包含 HTML 实例的 children 集合中

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 的父类)的成员

operator fun String.unaryPlus() {
children.add(TextElement(this))
}

  所以,在这里前缀 + 所做的事情是把一个字符串包装到一个 TextElement 实例中,并将其添加到 children 集合中,以使其成为标签树的一个适当的部分。

  所有这些都在上面构建器示例顶部导入的包 com.example.html 中定义。在最后一节中,你可以阅读这个包 的完整定义

作用域控制:@DslMarke(r 自 1.1 起)

  使用 DSL 时,可能会遇到上下文中可以调用太多函数的问题。我们可以调用 lambda 表达式内部每个可用的隐式接收者的方法,因此得到一个不一致的结果,就像在另一个 head 内部的 head 标记那样
html {
head {
head {} // 应该禁止
}
// ......
}

  在这个例子中,必须只有最近层的隐式接收者 this@head 的成员可用;head() 是外部接收者 this@html 的成员,所以调用它一定是非法的。

  为了解决这个问题,在 Kotlin 1.1 中引入了一种控制接收者作用域的特殊机制。

  为了使编译器开始控制标记,我们只是必须用相同的标记注解来标注在 DSL 中使用的所有接收者的类型。例如,对于 HTML 构建器,我们声明一个注解 @HTMLTagMarker

 @DslMarker
annotation class HtmlTagMarker

  如果一个注解类使用 @DslMarker 注解标注,那么该注解类称为 DSL 标记。

  在我们的 DSL 中,所有标签类都扩展了相同的超类 Tag 。只需使用 @HtmlTagMarker 来标注超类就足够了,之后,Kotlin 编译器会将所有继承的类视为已标注

 @HtmlTagMarker
abstract class Tag(val name: String) { ...... }

  我们不必用 @HtmlTagMarker 标注 HTML 或 Head 类,因为它们的超类已标注过

class HTML() : Tag("html") { ...... }
class Head() : Tag("head") { ...... }

  在添加了这个注解之后,Kotlin 编译器就知道哪些隐式接收者是同一个 DSL 的一部分,并且只允许调用最近层 的接收者的成员

html {
head {
head { } // 错误:外部接收者的成员
}
// ......
}

  请注意,仍然可以调用外部接收者的成员,但是要做到这一点,你必须明确指定这个接收者

html {
head {
this@html.head { } // 可能
}
// ......
}

  

com.example.html 包的完整定义

  这就是 com.example.html 包的定义(只有上面例子中使用的元素)。它构建一个 HTML 树。代码中大量使 用了扩展函数和带有接收者的 lambda 表达式。

  请注意,@DslMarker 注解在 Kotlin 1.1 起才可用

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更多语言结构——>类型安全的构建器的更多相关文章

  1. 《Mybatis 手撸专栏》第9章:细化XML语句构建器,完善静态SQL解析

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 你只是在解释过程,而他是在阐述高度! 如果不是长时间的沉淀.积累和储备,我一定也没有 ...

  2. 06. Go 语言结构体

    Go语言结构体(struct) Go 语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型.Go 语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性. Go 语言中的类 ...

  3. KOTLIN开发语言文档(官方文档) -- 2.基本概念

    网页链接:https://kotlinlang.org/docs/reference/basic-types.html 2.   基本概念 2.1.  基本类型 从可以在任何变量处理调用成员函数和属性 ...

  4. ArcGIS Pro 简明教程(4)工具和模型构建器

    ArcGIS Pro 简明教程(4)工具和模型构建器 by 李远祥 工具箱中的工具 ArcGIS Pro 在1.3版本基本上已经继承了ArcMap的所有工具,而且会不断加入一些它自身才有的工具,例如适 ...

  5. 设计模式---对象创建模式之构建器模式(Builder)

    一:概念 Builder模式也叫建造者模式或者生成器模式,是由GoF提出的23种设计模式中的一种.Builder模式是一种对象创建型模式之一,用来隐藏复合对象的创建过程,它把复合对象的创建过程加以抽象 ...

  6. Java 语言结构【转】

    Java 语言结构 基础:包(Package).类(Class)和对象(Object) 了解 Java 的包(Package).类(Class)和对象(Object)这些基础术语是非常重要的,这部分内 ...

  7. ES-自然语言处理之中文分词器

    前言 中文分词是中文文本处理的一个基础步骤,也是中文人机自然语言交互的基础模块.不同于英文的是,中文句子中没有词的界限,因此在进行中文自然语言处理时,通常需要先进行分词,分词效果将直接影响词性.句法树 ...

  8. 【做中学】第一个 Go 语言程序:漫画下载器

    原文地址: 第一个 Go 语言程序:漫画下载器: https://schaepher.github.io/2020/04/11/golang-first-comic-downloader 之前学了点 ...

  9. (转)PHP的语言结构和函数的区别

    相信大家经常看到对比一些PHP应用中,说用isset() 替换 strlen(),isset比strlen执行速度快等. 例子: if ( isset($user) ) { //do some thi ...

  10. Java数据持久层框架 MyBatis之API学习九(SQL语句构建器详解)

    对于MyBatis的学习而言,最好去MyBatis的官方文档:http://www.mybatis.org/mybatis-3/zh/index.html 对于语言的学习而言,马上上手去编程,多多练习 ...

随机推荐

  1. 【Spring Data JPA】05 方法名限定查询

    方法名限定查询 方法名限定查询是对JPQL的再封装 按照SpringData提供的方法名定义方法,不需要配置JPQL语句即可完成查询 在IDEA中都有相应的提示 他会按照方法字符判断 public C ...

  2. python高性能计算:cython入门代码

    三种实现的对比: (1)纯python x.py def is_prime(num): for j in range(2, num): if (num%j)==0: return False retu ...

  3. 【转载】 优必选悉尼 AI 研究院何诗怡:基于课程学习的强化多标签图像分类算法 | 分享总结

    原文地址: https://baijiahao.baidu.com/s?id=1603057342167437458&wfr=spider&for=pc 来源"雷锋网&quo ...

  4. DebugView使用

    操作说明 要知道怎么操作debugview,首先得下载下来.https://docs.microsoft.com/en-us/sysinternals/downloads/debugview 配置过滤 ...

  5. Redis中的Hash类型常用命令

    一.hset命令作用:设置hash类型值:格式:hset key field value案例:192.168.0.111:0>hset product name 苹果"1" ...

  6. java Hutool工具类之Excel的操作

    1.背景 程序中上传下载excel是家常便饭,因此hutool给我们提供了非充强大的工具类,使用如下...... 2.使用 官方地址:https://hutool.cn/docs/#/poi/Exce ...

  7. 下一代浏览器和移动自动化测试框架:WebdriverIO

    1.介绍 今天给大家推荐一款基于Node.js编写且号称下一代浏览器和移动自动化测试框架:WebdriverIO 简单来讲:WebdriverIO 是一个开源的自动化测试框架,它允许测试人员使用 No ...

  8. 【牛客刷题】HJ68 成绩排序

    题目链接 这题本身就是一个排序题,按照学生成绩排序,成绩一样的按照输入的前后顺序排. 如果用Java,那么利用ArrayList能很轻松的完成: import java.util.ArrayList; ...

  9. RabbitMQ 基础概念与架构设计及工作机制学习总结

    什么是RabbitMQ MQ全称为Message Queue,即消息队列. 它也是一个队列,遵循FIFO原则 .RabbitMQ则是一个开源的消息中间件,由erlang语言开发,基于AMQP协议实现的 ...

  10. Hexo-GitHub部署魔改第一步-config

    Hexo-GitHub部署魔改第一步_config.yml 1. config.yml # Hexo Configuration ## Docs: https://hexo.io/docs/confi ...