通过使用命名得当的函数作为构建器,结合带有接收者的函数字面值,可以在 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. 【转载】AI的剥削:肯尼亚工人训练ChatGPT,看大量有害内容心理受伤——AI新时代下剥削

    原文地址: https://mbd.baidu.com/newspage/data/landingsuper?context=%7B%22nid%22%3A%22news_90623597686514 ...

  2. 使用tensorbaoardx报错——Descriptors cannot not be created directly. If this call came from a _pb2.py file, your generated code is out of date and must be regenerated with protoc >= 3.19.0.

    运行代码报错: from tensorboardX import SummaryWriter 报错内容: 发生异常: TypeError Descriptors cannot not be creat ...

  3. ollama安装和运行llama3.1 8b

    ollama安装和运行llama3.1 8b conda create -n ollama python=3.11 -y conda activate ollama curl -fsSL https: ...

  4. Java基础之数值类型之间的转换

    经常需要将一种数值类型转换为另一种数值类型.下图 给出了数值类型之间的合法 转换. 在图中有 6 个实心箭头,表示无信息丢失的转换:有 3 个虚箭头, 表示可能有精度 损失的转换. 例如,123 45 ...

  5. stm32中NVIC如何配置?

    1.NVIC优先级分组 2.初始化NVIC // NVIC优先级分组 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // NVIC初始化 NVIC_I ...

  6. ABC304Ex Constrained Topological Sort 题解

    https://atcoder.jp/contests/abc304/tasks/abc304_h [CSP-S 2023] 种树后半部分的加强版 对于边 \((u,v)\),不妨令 $r[u]$ 对 ...

  7. 使用FastAPI来开发项目,项目的目录结构如何规划的一些参考和基类封装的一些处理

    使用FastAPI开发项目时,良好的目录结构可以帮助你更好地组织代码,提高可维护性和扩展性.同样,对基类的封装,也可以进一步减少开发代码,提供便利,并减少出错的几率. 下面是一个推荐的目录结构示例: ...

  8. 配置 Windows Boot Manager

    配置 Windows Boot Manager 通常需要使用 bcdedit 命令,这是一个命令行工具,用于管理 Boot Configuration Data (BCD) 存储.BCD 存储包含了启 ...

  9. Linux 主流桌面环境

    GNOME KDE Xfce Ubuntu 使用 GNOME 作为桌面环境. 基于 KDE Plasma 开发的 Ubuntu 发行版:Kubuntu 基于 Xfce 开发的 Ubuntu 发行版:X ...

  10. wget 提示 "无法验证 xxxx.xxx 的由 “xxx” 颁发的证书: 无法本地校验颁发者的权限。"

    有一天在使用 wget 下载文件时,出现了无法验证证书的提示: $ wget https://github.com/zayronxio/Mkos-Big-Sur/releases/download/0 ...