《Kotlin实战》-第11章:DSL构建

news2024/11/8 10:06:03

第十一章 DSL构建

这一章会讨论怎样使用领域特定语言DSL(Domain-Specific-Language)为Kotlin类设计更有表现力、更符合语言习惯的API。
Kotlin DSL依赖于众多的语言特性,本章会完整讨论两个:带接收者的lambda(第五章介绍过)和invoke约定。

11.1 从API到DSL

我们的目标是尽可能地让代码具有最佳可读性和可维护性。因此不仅要关注单独的类,更重要的是要关注交互发生的接口-也就是类的API。
一个API整洁的标准:

  • 需要能够让读者清楚地知道在代码中发生了什么。这可以通过选择良好的名称和概念来做到。
  • 代码需要看起来整洁,极少使用浮夸的代码且不存在多余的语法。这也是本章关注的目标。

Kotlin允许构建整洁API的功能的代表包括:

  • 扩展函数
  • 中缀调用
  • lambda简明语法
  • 运算符重载

DSL比由单独方法构造出的API更具表现力且更适宜工作。
Kotlin的DSL是完全静态类型的。

11.1.1 领域特定语言的概念

有两个相对概念:

  • 通用编程语言(GPL):有一系列足够完善的能力来解决几乎所有能被计算机解决的问题。
  • 领域特定语言(DSL):专注在特定任务或者领域上,并放弃与该领域无关的功能。

常见的DSL有SQL和正则表达式。
这类DSL的优点或者说特点:

  • 通过减少提供的功能来有效完成目标
  • 更趋向于声明式。

有一个缺点:很难与使用通用编程语言的宿主应用程序结合起来使用。

要解决上述DSL的缺点,并且保留优点,目前有个概念是内部DSL。

11.1.2 内部DSL

内部DSL是用通用编程语言编写的程序的一部分,使用语言的特定方式,保留具有独立语法的DSL的主要优点。

示例为Exposed框架,Kotlin编写的用来访问数据库的框架。请自行搜索查看demo。

11.1.3 DSL的结构

DSL与普通的API之间没有明确定义的边界,但DSL中经常会出现一个通常在其他API中不存在的特征:结构或者说是文法。
在Kotlin DSL中,结构通常是通过嵌套的lambda表达式或链式方法调用来创建。
这种文法使得能够将一个内部DSL称作一门语言。

DSL结构的一个好处是允许在多个函数调用之间重用一个上下文。

11.1.4 使用内部DSL构建HTML

示例:

fun createSimpleTable() = createHTML().
    table{
        tr{
            td{ +"cell" }
        }
    }
//对应的HTML
<table>
    <tr>
        <td>cell</td>
    </tr>
</table>

示例是kotlinx.html库里的代码,下面一节会讲解说明。

11.2 构建结构化的API:DSL中带接受者的lambda
11.2.1 带接受者的lambda和扩展函数类型

1.扩展函数类型
扩展函数类型:在声明的时候,将普通函数类型签名中的一个参数(类型)移到括号前面,并用一个.将它与其他的参数(类型)分隔开。
示例:

StringBuilder.() -> Unit
String.(int,int) -> Unit
//相当于
(StringBuilder) -> Unit
(String,int,int) -> Unit

其中,(StringBuilder)叫做接受者类型,传递给lambda的这个类型值就叫做接受者对象。

之所以使用扩展函数类型是因为同之前章节介绍的扩展函数一样,最明显的是调用方式发生了变化,由普通的builderAction(sb)变为sb.builderAction()。
此外就是在函数块内不需要显式的修饰符就可以访问一个外部类型的成员。

示例:

//以普通lambda为参数
fun buildString(builderAction:(StringBuilder) -> Unit):String{
    val sb = StringBuilder()
    builderAction(sb)
    return sb.toString()
}
//调用
val s = buildString{
    //it指向StirngBuilder实例
    it.append("Hello,")
    it.append("world")
}
//以扩展函数类型lambda为参数
fun buildString(builderAction:StringBuilder.() -> Unit):String{
    val sb = StringBuilder()
    sb.builderAction()
    return sb.toString()
}
//调用
val s = buildString{
   //this指向StirngBuilder实例
   this.append("Hello,")
   append("world")//可以省略this
}

2.扩展函数变量
可以声明一个扩展函数类型变量,然后可以像扩展函数一样调用它,也可以将它作为参数传递给一个期望带接受者的lambda为参数的函数。
示例:

//声明一个变量
val appendExcl:StringBuilder.() -> Unit = { this.append("!") }
val stringBuilder = StringBuilder("Hi")
//当扩展函数样调用
stringBuilder.appendExcel()
println(stringBuilder)
//当lambda类型参数调用
println(buildString(appendExcl))

3.apply和with的实现
标准库中的apply和with函数都是对提供给它的接收者调用扩展函数类型的参数。
不同的是apply被声明为这个接受者的扩展函数,而with是把接受者作为第一个参数。
还有就是apply返回的是接受者本身,with返回的是调用lambda后的结果
标准库中的实现代码:

inline fun <T> T.apply(block:T.() -> Unit):T{
    block()//可以省略this
    return this
}

inline fun <T,R> with(receiver:T,block:T.()->R):R = 
    receiver.block()

上面的buildString可以用apply进一步被改写为:

fun buildString(builderAction:StringBuilder.()->Unit):String = 
    StringBuilder().apply(builderAction).toString
11.2.2 在HTML构建器中使用带接受者的lambda

用于HTML的Kotlin构建器通常被叫做HTML构建器,它代表了一个更普遍的概念叫做类型安全的构建器。
直接看示例及注释解析:

//一个标签类
open class Tag(val name:String){
    //嵌套标签列表
    private val children = mutableListOf<Tag>()
    //每一个子标签都要做的事,初始化,添加进子标签列表
    protected fun <T:Tag> doInit(child:T,init:T.() -> Unit){
        child.init()
        children.add(child)
    }
    override fun toString() = "<$name>${children.joinToString("")</$name>"
}
//继承标签类,同时拥有一个可以创建子标签的函数
class TABLE:Tag("table"){
    //这个子标签的函数特点是以子标签的扩展函数为参数类型,这样就限制了子标签的创建位置
    fun tr(init:TR.() -> Unit) = doInit(TR(),init)
}

class TR:Tag("tr"){
    fun td(init:TD.() -> Unit) = doInit(TD(),init)
}

class TD:Tag("td")

fun table(init:TABLE.() -> Unit) = TABLE().apply(init)

fun createTable() = 
    table{
        tr{//本质是TABLE().tr,只是接受者被省略了
            td{}
        }
        tr{}
    }

//调用
println(createTable())
>>>>>>
<table><tr><td></td></tr><tr></tr></table>

//另一个使用方式
fun createAnotherTable() = table{
    for(i in 1..2){
        tr{
            td{}
        }
    }
}

如示例所见带接收者的lambda是构建DSL的好工具,可以在代码块中改变命名解析上下文,也就是可以在API中创建结构。这是区分平铺的方法调用序列与DSL的关键特征之一。
上面示例里createAnotherTable函数展示了这种内部DSL的极大的拓展性和自由性,像不像angularJS或者react native中的那种逻辑数据和页面共生的写法。Android中马上要推出正式版本的compose也是此种写法,底层原理应与此类似。

11.2.3 Kotlin构建器:促成抽象和重用

本小节以kotlinx.html库为示例演示了如何重构改进代码,没有新知识点,在此不赘述。

11.3 使用“invoke”约定构建更灵活的代码块嵌套

invoke约定允许把自定义类型的对象当做函数一样调用。

11.3.1 “invoke”约定:像函数一样可以调用的对象

先看示例:

//声明
class Greeter(val greeting:String){
    operator fun invoke(name:String){
        println("$greeting,$name")
    }
}
//调用
val newGreeter = Greeter("Haha")
newGreeter("Hehe")
>>>>>
Haha Hehe

上面的表达式newGreeter(“Hehe”)会被编译成newGreeter.invoke(“Hehe”)。
就如同一个普通约定一样:提供了一种方法,用更简洁清晰的表达式替换了冗长的表达式。
invoke方法没有限制任何特殊签名,可以给它定义任意数量的参数和任意的返回类型。

11.3.2 “invoke”约定和函数式类型

本书前面部分说过invoke,像lambda?.invoke()调用一个可空函数类型的变量。
这就是invoke约定的应用。
当将lambda作为函数调用时,这种操作被转换成一次invoke的调用。
知道了这个,就可以明白这样一种优化代码的方式:将复杂lambda代码拆分成多个方法,同时仍然允许将它与接收函数类型参数的函数一起使用。
要做到这一点,可以定义一个实现了函数类型接口的类。

示例:

data class Person(val name:String,val age:Int)

class PersonPredicate(val name:String):(Person)->Boolean{

    override fun invoke(person:Person):Boolean{
        return person.name == name && person.isAlive()
    }
    //将更多的判断逻辑抽取出来成为单独方法
    private fun Person.isAlive():Boolean{
        return age < 100
    }
}
//调用
val list = listOf(Person("aa",10),Person("aa",120),Person("bb",10))
val predicate = PersonPredicate("aa")
println(list.filter(predicate))
>>>>>
[Person(name=aa, age=10)]
11.3.3 DSL中的“invoke”约定:在Gradle中声明依赖

如果想同时支持嵌套代码块结构和扁平调用结构,如下所示:

dependencies.compile("junit:junit:4.11")
dependencies{
    compile("junit:junit:4.11")
}

这需要把invoke约定和普通函数以及带接受者的扩展函数类型结合起来,如下所示:

class DependencyHandler{
    //普通函数
    fun compile(coordinate){ ... }
    //invoke函数,这里注意body()其实是this.body()
    operator fun invoke(body:DependencyHandler.() -> Unit){ body() }
}

这样一段短小的代码片段,仅仅重定义了一个invoke方法,就显著地增加了DSL API的灵活性。这个模式很通用,稍加修改就可以在自己的代码里应用。

11.4 实践中的Kotlin DSL

本节就是展示一些实际的DSL构建案例,没有新知识点。

11.4.1 把中缀调用链接起来:测试框架中的“should”

kotlintext框架里有如下写法:

s should startWith("kot")
s should start with "kot"

上面第一种使用了中缀调用,以及一些特别的命名:

//中缀调用
infix fun <T> T.should(matcher:Matcher<T>) = matcher.test(this)

interface Matcher<T>{
    fun test(value:T)
}
//这里将类名用小写字母开头是为了调用时更加像日常用语习惯
class startWith(val prefix:String):Matcher<String>{
    override fun test(value:String){
        if(!value.startsWith(prefix)){
            throw AssertionError("wrong ...")
        }
    }
}

上面示例中的第二种调用稍微复杂一点,用了两个中缀调用及object类型:

//等同的写法是
s.should(start).with("kot")

//声明
//object不是用来给函数传递任何数据的,而是DSL文法的一部分
object start

//should在这里是一个特殊的重载,使用了start对象作为参数类型并返回一个中间包装器
infix fun String.should(x:start):StartWrapper = StartWrapper(this)

//包装器类型里又声明了一个中缀调用函数
class StartWrapper(val value:String){
    infix fun with(prefix:String) = 
        if(!value.startsWith(prefix)) throw ssertionError("wrong ...")
}
11.4.2 在基本数据类型上定义扩展:处理日期

先看下面代码思考下如何做到:

val yesterday = 1.days.ago
val tomorrow = 1.days.fromNow

想到了吗,这里是对Int类型增加了扩展属性,然后又增加了一次扩展:

//Period是JDK8 中用来表示两个日期之间间隔的类型
val Int.days:Period
    get() = Period.ofDays(this)
val Period.ago:LocalDate
    get() = LocalDate.now() - this //使用运算符语法调用LocalDate.minus
val Period.fromNow:LocalDate
    get() = LocalDate.now() + this //使用运算符语法调用LocalDate.plus
//调用
println(1.days.ago)
println(1.days.fromNow)
>>>>>
2016-08-16
2016-08-18
11.4.3 成员扩展函数:为SQL设计的内部DSL

成员扩展:在类中声明扩展函数和扩展属性。这样的属性或函数既是它容器类的成员,也是某些其他类型的扩展。

本小节以Exposed框架为例,展示成员扩展函数的应用。
先看一个使用代码:

//语义很明确,创建一个简单的带两列的Country表
object Country:Table(){
    val id = integer("id").autoIncrement().primaryKey()
    val name = varchar("name",50)
}

下面来看Table的声明:

class Table{
    fun integer(name:String):Column<Int>
    fun varchar(name:String,length:Int):Column<String>
    //扩展函数,将该列设成表的主键,省略了实现
    fun <T> Column<T>.primaryKey():Column<T>
    //扩展函数,只有整型值能够自增,省略了实现
    fun Column<Int>.autoIncrement():Column<Int>
}

如上所示,这些扩展函数式Table类的成员,也就意味着不能在这个类作用域之外去使用他们。
同时在自增函数里有一点做的很好的是限制了接受者类型,只有数字列才可以自增。

成员扩展的缺点在于其扩展性的缺失:它们是属于类的,不能另外定义新的成员扩展。
例如想要增加新的列属性的时候,必须修改Table类的定义来为新属性增加成员扩展函数,而不能像普通扩展那样,不改动原始类添加必要的声明。

11.4.4 Anko:动态创建Android UI

本小节的示例其实和前面提到的创建HTML的代码大同小异,都是利用了带接收者的lambda这一种来简化代码。在此不赘述。

11.5 小结

略。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2235738.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GISBox VS ArcGIS:分别适用于大型和小型项目的两款GIS软件

在现代地理信息系统&#xff08;GIS&#xff09;领域&#xff0c;有许多大家耳熟能详的GIS软件。它们各自具有独特的优势&#xff0c;适用于不同的行业需求和使用场景。在众多企业和开发者面前&#xff0c;如何选择合适的 GIS 软件成为了一个值得深入思考的问题。今天&#xff…

【Spring】Spring Web MVC基础入门~(含大量例子)

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 一&#xff1a;什么是Spring Web MVC 1&#xff1a;Servlet 2&#xff1a;总结 二&#xff1a;MVC …

数组类算法【leetcode】

704. 二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 二分查找 用于有序数组中&#xff0c;没有重复的数组。…

24/11/7 算法笔记 PCA主成分分析

假如我们的数据集是n维的&#xff0c;共有m个数据(x,x,...,x)。我们希望将这m个数据的维度从n维降到k维&#xff0c;希望这m个k维的数据集尽可能的代表原始数据集。我们知道数据从n维降到k维肯定会有损失&#xff0c;但是我们希望损失尽可能的小。那么如何让这k维的数据尽可能表…

wxWidgets布局管理及XRC界面使用解惑

wxWidgets布局&#xff0c;通常就是wxBoxSizer&#xff0c;当然还有别的Sizer&#xff0c;就像Qt中的Layout一样&#xff0c;各种布局管理类。只是如今的wxWidgets尚未发展起来&#xff0c;一般其他的你用的比较少&#xff0c;因为这个最简单&#xff0c;最容易布局&#xff0c…

【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)

博主说明&#xff1a;本文项目编号 T 062 &#xff0c;文末自助获取源码 \color{red}{T062&#xff0c;文末自助获取源码} T062&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析…

【循环引用及格式化输出】

垃圾回收机制 当一个值在内存中直接引用跟间接引用的量为0时&#xff0c;&#xff08;即这个值没有任何入口可以找到它&#xff09;那么这个值就会被清空回收♻️&#xff0c;释放内存空间&#xff1b; 列表在内存中的存储方式 1&#xff09;引用计数的两种方式 x "ea…

31.7K+ Star!AgentGPT:一个在浏览器中运行的Agent

AgentGPT 简介 AgentGPT[1] 是一个可以让你在浏览器中组装、配置和部署自主AI代理的项目。你可以为你的自定义AI命名,并让它去实现任何你想象中目标。它将尝试通过思考要执行的任务、执行它们并从结果中学习来达成目标。 项目特点 主要特点 自主AI代理:用户可以自定义AI并赋…

【大模型】通过Crew AI 公司的崛起之路学习 AI Agents 的用法

AI 技术的迅猛发展正以前所未有的速度重塑商业格局&#xff0c;而 AI Agents&#xff0c;作为新一代的智能自动化工具&#xff0c;正逐步成为创新型公司的核心力量。在本文中&#xff0c;我们将探讨如何利用 AI Agents 构建一家 AI 驱动的公司&#xff0c;并详细了解 Crew AI 创…

【Uniapp】Uniapp Android原生插件开发指北

前言 在uniapp开发中当HBuilderX中提供的能力无法满足App功能需求&#xff0c;需要通过使用Andorid/iOS原生开发实现时&#xff0c;或者是第三方公司提供的是Android的库&#xff0c;这时候可使用App离线SDK开发原生插件来扩展原生能力。 插件类型有两种&#xff0c;Module模…

网页版五子棋——用户模块(服务器开发)

前一篇文章&#xff1a;网页版五子棋—— WebSocket 协议-CSDN博客 目录 前言 一、编写数据库代码 1.数据库设计 2.配置 MyBatis 3.创建实体类 4.创建 UserMapper 二、前后端交互接口 1.登录接口 2.注册接口 3.获取用户信息 三、服务器开发 1.代码编写 2.测试后端…

Jenkins声明式Pipeline流水线语法示例

系列文章目录 docker搭建Jenkins2.346.3版本及常用工具集成配置(ldap、maven、ansible、npm等) docker安装低版本的jenkins-2.346.3,在线安装对应版本插件失败的解决方法 文章目录 系列文章目录jenkins流水线基础1、pipeline1.1、什么是pipeline&#xff1f;1.2、为什么使用pi…

【NLP】使用 SpaCy、ollama 创建用于命名实体识别的合成数据集

命名实体识别 (NER) 是自然语言处理 (NLP) 中的一项重要任务&#xff0c;用于自动识别和分类文本中的实体&#xff0c;例如人物、位置、组织等。尽管它很重要&#xff0c;但手动注释大型数据集以进行 NER 既耗时又费钱。受本文 ( https://huggingface.co/blog/synthetic-data-s…

【数据集】【YOLO】【目标检测】道路裂缝数据集 5466 张,YOLO/VOC格式标注!

数据集介绍 【数据集】道路裂缝数据集 5466 张&#xff0c;目标检测&#xff0c;包含YOLO/VOC格式标注。数据集中包含一种分类&#xff0c;检测范围城市道路裂缝、高速道路裂缝、乡村道路裂缝。 戳我头像获取数据&#xff0c;或者主页私聊博主哈~ 一、数据概述 道路裂缝检测…

C++用string实现字符串相加

. - 力扣&#xff08;LeetCode&#xff09; -》》》》》题目链接 实现思路&#xff1a;计算数字符串长度并用数组的方式计算出字符位置&#xff0c;用字符的ask码‘0’计算出字符本身。 class Solution { public:string addStrings(string num1, string num2) {string str;int…

easyexcel实现自定义的策略类, 最后追加错误提示列, 自适应列宽,自动合并重复单元格, 美化表头

easyexcel实现自定义的策略类, 最后追加错误提示列, 自适应列宽,自动合并重复单元格, 美化表头 原版表头和表体字体美化自动拼接错误提示列自适应宽度自动合并单元格使用Easyexcel使用poi导出 在后台管理开发的工作中,离不开的就是导出excel了. 如果是简单的导出, 直接easyexce…

brainpy 动力学编程基础

文章参考&#xff1a; 《神经计算建模实战——基于brainpy》 吴思 【brainpy学习笔记】基础知识2(动力学模型的编程基础)-CSDN博客 Brainpy手册 文章目录 积分器&#xff1a;定义ODE函数数值积分方法 更新函数和动力系统计算介绍什么是brainpy.DynamicalSystem&#xff1f;如…

高级图像处理工具

图像处理-高级 1、功能概览 随着社交媒体的普及和个人创作需求的增长&#xff0c;图像处理成为了日常生活中不可或缺的一部分。无论是专业的设计师还是爱好者&#xff0c;都需要一款强大的工具来帮助他们完成各种任务。今天&#xff0c;我们将介绍一款基于Python开发的高级图…

【Zookeeper集群搭建】安装zookeeper、zookeeper集群配置、zookeeper启动与关闭、zookeeper的shell命令操作

目录 一、安装Zookeeper 二、配置Zookeeper集群 三、Zookeeper服务的启动与关闭 四、Zookeeper的shell操作 前情提要&#xff1a;延续上篇【Hadoop和Hbase集群配置】继续配置Zookeeper&#xff0c;开启三台虚拟机Hadoop1、Hadoop2、Hadoop3&#xff0c;进入终端&#xff0c…

Transformer和BERT的区别

Transformer和BERT的区别比较表&#xff1a; 两者的位置编码&#xff1a; 为什么要对位置进行编码&#xff1f; Attention提取特征的时候&#xff0c;可以获取全局每个词对之间的关系&#xff0c;但是并没有显式保留时序信息&#xff0c;或者说位置信息。就算打乱序列中token…