【Kotlin精简】第5章 简析DSL
1 DSL是什么?
Kotlin 是一门对 DSL 友好的语言,它的许多语法特性有助于 DSL 的打造,提升特定场景下代码的可读性和安全性。本文将带你了解 Kotlin DSL 的一般实现步骤,以及如何通过 @DslMarker , Context Receivers 等特性提升 DSL 的易用性。
DSL 全称是 Domain Specific Language,即领域特定语言。顾名思义 DSL 是用来专门解决某一特定问题的语言,比如我们常见的 SQL 或者正则表达式等,DSL 没有通用编程语言(Java、Kotlin等)那么万能,但是在特定问题的解决上更高效。
2 Gradle Kotlin DSL的优点和使用
Gradle Kotlin DSL是Gradle 5.0引入的一种新型的Gradle脚本语言,作为Groovy语言的替代方案。
官方文档中提到,Kotlin DSL具有如下的优点:
- 类型安全:编写
Gradle脚本时,可以进行静态类型检查,这样可以保证更高的代码质量和更好的可维护性; - 代码提示:
Kotlin语言具有良好的编码体验,比如IDE可以提示代码补全、语法错误等,这些在Groovy语言中不易得到; - 使用简单:
Kotlin是一种现代化的语言,语法易懂,学习成本低; - 高效性:
Gradle使用Kotlin编写的DSL脚本会比同样的Groovy脚本快2~10倍。
创作一套全新新语言的成本很高,所以很多时候我们可以基于已有的通用编程语言打造自己的 DSL,比如日常开发中我们将常见到 gradle 脚本 ,其本质就是来自 Groovy 的一套 DSL:
android {compileSdkVersion 28defaultConfig {applicationId "com.my.app"minSdkVersion 24targetSdkVersion 30versionCode 1versionName "1.0"testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}
}
build.gradle 中我们可以用大括号表现层级结构,使用键值对的形式设置参数,没有多余的程序符号,非常直观。如果将其还原成标准的 Groovy 语法则变成下面这样,是下面这样,在可读性上的好坏立判:
Android(30,DefaultConfig("com.my.app",24,30,1,"1.0","android.support.test.runner.AndroidJUnitRunner")
),BuildTypes(Release(false,getDefaultProguardFile('proguard-android-optimize.txt'),'proguard-rules.pro')
)
除了 Groovy,Kotlin 也非常适合 DSL 的书写,正因如此 Gradle 开始推荐使用 kts 替代 gradle,其实就是利用了 Kotlin 优秀的 DSL 特性。
3 Kotlin DSL 及其优势
Kotlin 是 Android 的主要编程语言,因此我们可以在 Android 开发中发挥其 DSL 优势,提升特定场景下的开发效率。例如 Compose 的 UI 代码就是一个很好的示范,它借助 DSL 让 Kotlin 代码具有了不输于 XML 的表现力,同时还兼顾了类型安全,提升了 UI 开发效率。
3.1 一个简单DSL例子
在Kotlin中实现DSL构建要依靠这几样东西:
扩展函数;带接收者的 Lambda 表达式;在方法括号外使用Lambda
我们先来看一下一个DSL例子:
val person = person {name = "John"age = 25address {street = "Main Street"number = 42city = "London"}
}// 数据模型
data class Person(var name: String? = null,var age: Int? = null,var address: Address? = null)data class Address(var street: String? = null,var number: Int? = null,var city: String? = null)
要实现上面的语法糖,现在要做的第一件事就是创建一个新文件,将保持DSL与模型中的实际类分离。首先为Person类创建一些构造函数。看看我们想要的结果,看到Person的属性是在代码块中定义的。这些花括号实际上是定义一个lambda。这就是使用上面提到的三种Kotlin语言特征中的第一种语言特征的地方:在方法括号外使用Lambda。
如果一个函数的最后一个参数是一个lambda,可以把它放在方法括号之外。而当你只有一个lambda作为参数时,你可以省略整个括号。person {…}实际上与person({…})相同。这在我们的DSL中变得更简洁。现在来编写person函数的第一个版本。
// 数据模型
fun person(block: (Person) -> Unit): Person {val p = Person()block(p)return p
}
所以在这里我们有一个创建一个Person对象的函数。它需要一个带有我们在第2行创建的对象的lambda。当在第3行执行这个lambda时,我们期望在返回第4行的对象之前,该对象获得它所需要的属性。下面展示如何使用这个函数:
val person = person {it.name = "John"it.age = 25
}
由于这个lambda只接收一个参数,可以用它来调用person对象。这看起来不错,但还不够完美,如果在我们的DSL看到的东西。特别是当我们要在那里添加额外的对象层。这带来了我们接下来提到的Kotlin功能:带接受者的Lambda。
在person函数的定义中,可以给lambda添加一个接收者。这样只能在lambda中访问那个接收者的函数。由于lambda中的函数在接收者的范围内,则可以简单地在接收者上执行lambda,而不是将其作为参数提供。
fun person(block: Person.() -> Unit): Person {val p = Person()p.block()return p
}// 这实际上可以通过使用Kotlin提供的apply函数在一个简单的单行程中重写。
fun person(block: Person.() -> Unit): Person = Person().apply(block)
现在可以将其从DSL中删除:
val person = person {name = "John"age = 25
}
到目前为止,还差一个Address类,在我们想要的结果中,它看起来很像刚刚创建的person函数。唯一的区别是必须将它分配给Person对象的Address属性。为此,可以使用上面提到的三个Kotlin语言功能中的最后一个:扩展函数。
扩展函数能够向类中添加函数,而无需访问类本身的源代码。这是创建Address对象的完美选择,并直接将其分配给Person的地址属性。这是DSL文件的最终版本:
fun person(block: Person.() -> Unit): Person = Person().apply(block)fun Person.address(block: Address.() -> Unit) {address = Address().apply(block)
}
现在为Person添加一个地址函数,它接受一个Address作为接收者的lambda表达式,就像对person构造函数所做的那样。然后它将创建的Address对象设置为Person的属性:
val person = person {name = "John"age = 25address {street = "Main Street"number = 42city = "London"}
}
3.2 实现简单的UI布局
我们先来看下这个布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/tv"android:layout_width="match_parent"android:layout_height="match_parent"android:textSize="16sp"android:paddingTop="10dp" /></FrameLayout>
上面XML使用DSL写法如下:
context.FrameLayout {layout_width = match_parentlayout_height = wrap_contentTextView {layout_id = "tv"layout_width = match_parentlayout_height = match_parenttextSize = 16fpadding_top = 10}}
首先要定义一种声明方式来初始化对象,所以可以写一个基于Context的扩展函数
inline fun Context.FrameLayout(style: Int? = null,init: FrameLayout.() -> Unit
): FrameLayout {val frameLayout =if (style != null) FrameLayout(ContextThemeWrapper(this, style)) else FrameLayout(this)return frameLayout.apply(init)
}// 扩展View的layout_width、layout_height等属性,
// 其他属性这里不做详解,写法同layout_width、layout_height
inline var View.layout_width: Numberget() {return 0}set(value) {val w = if (value.dp > 0) value.dp else value.toInt()val h = layoutParams?.height ?: 0updateLayoutParams<ViewGroup.LayoutParams> {width = wheight = h}}inline var View.layout_height: Numberget() {return 0}set(value) {val w = layoutParams?.width ?: 0val h = if (value.dp > 0) value.dp else value.toInt()updateLayoutParams<ViewGroup.LayoutParams> {width = wheight = h}}
这里的init就是上面说的带接受者的lamba表达式拉,所以代码里去实现一个FrameLayout布局就可以这样子拉
context.FrameLayout {layout_width = match_parentlayout_height = wrap_content
}
而对于子控件,TextView举个栗子:
inline fun ViewGroup.TextView(style: Int? = null,init: AppCompatTextView.() -> Unit
): TextView {val textView =if (style != null) AppCompatTextView(ContextThemeWrapper(context, style)) else AppCompatTextView(context)return textView.apply(init).also { addView(it) }
}
这样一个简单的动态布局就出来了,没想象中那么高级,其实就是对扩展函数、高阶函数的运用。
3.3 小结
Kotlin DSL的好处,尤其是对View进行特定领域的处理的时候 很有用。
- 有着近似
XML的结构化表现力 - 较少的字符串,更多的强类型,更安全
- 可提取
linearLayoutParams这样的对象方便复用 - 在布局中同步嵌入
onClick等事件处理 - 如需要还可以嵌入
if,for这样的控制语句
4 DSL实现的原理
4.1 扩展函数(扩展属性)
package stringsfun String.lastChar(): Char = this.get(this.length - 1)

4.2 lambda使用
lambda 表达式定义:

高阶函数:高阶函数就是以另一个函数作为参数或返回值的函数。

Kotlin 的 lambda 有个规约:如果 lambda 表达式是函数的最后一个实参,则可以放在括号外面,并且可以省略括号
person.maxBy({ p:Person -> p.age })// 可以写成
person.maxBy(){p:Person -> p.age
}// 更简洁的风格:
person.maxBy{p:Person -> p.age
}
带接收者的 lambda:


想一想 File就是带接受者,说明这个lambda的对象是File。
4.3 中缀调用

中缀调用是实现类似英语句子结构 DSL 的核心。
4.4 invoke 约定

invoke约定的作用:它的作用就是让对象像函数一样调用方法。

class DependencyHandler{//编译库fun compile(libString: String){Logger.d("add $libString")}//定义invoke方法operator fun invoke(body: DependencyHandler.() -> Unit){body()}
}//我们有下面的3种调用方式:
val dependency = DependencyHandler()
//调用invoke
dependency.invoke {compile("androidx.core:core-ktx:1.6.0")
}
//直接调用
dependency.compile("androidx.core:core-ktx:1.6.0")
//带接受者lambda方式
dependency{compile("androidx.core:core-ktx:1.6.0")
}
5 总结
Kotlin DSL 是一种强大的工具,可以帮助我们编写更简洁、优雅的代码。通过使用 Kotlin DSL,我们可以提高代码的可读性、灵活性和类型安全性。当然 Android 中 DSL 远不止这些使用场景 ,但是实现思路都是相近的,最后再来一起回顾一下:
- DSL 是什么?
DSL 是一种针对特殊编程场景的语言或范式,它处理效率更高,且表达式更为专业。
例如 SQL、HTML、正则表达式等。 - Kotlin 如何支持 DSL
通过 扩展函数、带接收者的函数类型等来支持使用 DSL。 - Kotlin 自定义 DSL 的优势
提供一套编程风格,可以简化构建一些复杂对象的代码,提高简洁程度的同时,具备很高的可读性。 - Kotlin 自定义 DSL 的缺点
构造代码较为复杂,有一定上手难度,非必要不使用。
Tips: 对于顶级的Android发烧友,或者是Kotlin学习爱好者可以深度去挖掘DSL,或者是高级的Kotlin语法糖。注意对于在职场打拼的各位朋友们,还是那句话:学值得变现的知识点,并且要等机会来变现,从这个角度,Kotlin会用就可以了,不一定要非要死磕语法糖。切记。职场和自由职业free style 学习的东西是不一样的。
相关文章:
【Kotlin精简】第5章 简析DSL
1 DSL是什么? Kotlin 是一门对 DSL 友好的语言,它的许多语法特性有助于 DSL 的打造,提升特定场景下代码的可读性和安全性。本文将带你了解 Kotlin DSL 的一般实现步骤,以及如何通过 DslMarker , Context Receivers 等…...
2021年06月 Python(一级)真题解析#中国电子学会#全国青少年软件编程等级考试
Python编程(1~6级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 下列程序运行的结果是? s hello print(sworld)A: sworld B: helloworld C: hello D: world 答案:…...
MySQL执行计划分析
执行计划中的常见的列的解释: type system/const :用户主键索引或者唯一索引查询时,只能匹配 1 条数据。一般可以对 sql 查询语句优化成一个常量,那么 type 一般就是 system 或者 const,system 是 const 的一个特例&…...
【数据结构与算法】Snowflake雪花算法Java实现
Snowflake产生的ID由 64 bit 的二进制数字组成,被分成了4个部分,每一部分存储的数据都有特定的含义: 第 0 位: 符号位(标识正负),始终为 0;第 1~41 位 :一共 41 位&…...
重要功能更新:妙手正式接入SHEIN供货模式(OBM)店铺,赋能卖家把握出海新机遇!
继接入SHEIN平台模式店铺之后,妙手ERP积极响应卖家需求,正式接入SHEIN供货模式(OBM)店铺,并支持产品采集、批量刊登、产品管理等功能,帮助跨境卖家快速上品、高效运营,把握出海新机遇。 SHEIN供…...
和鲸ModelWhale与中科可控X系列异构加速服务器完成适配认证,搭载海光芯片,构筑AI算力底座
AIGC 时代,算力作为新型生产力,是国家和企业构建竞争优势的关键。而随着传统计算方式无法满足新时代激增的算力需求,计算场景的多元化和计算应用的复杂化推动了 CPUGPU 异构平台的加速组建。在此全球激烈角逐的大趋势下,我国信创产…...
Vue单文件组件
一、.vue文件 我们使用Vue的单文件组件的时候,一个.vue文件就是一个组件。 例如我们创建一个School组件: 二、组件的结构 我们编写网页代码的时候有HTML结构、CSS样式、JS交互。 组件里也是同样存在这三种结构的: <template><d…...
轻松理解 Transformers(1):Input部分
编者按:Transformers 是人工智能领域近年来最引人瞩目的技术之一,它为语言生成模型的发展做出了巨大的贡献。随着大语言模型(LLM)的兴起,公众对其背后的技术原理也越来越感兴趣。但是由于Transformers本身具有一定的复…...
PHP MySQL 交互 笔记/练习
PHP 与 MySQL 交互 交互函数 函数名作用mysqli_connect()与MySQL 数据库建立连接。mysqli_close()关闭与MYSQL 数据库建立的连接。mysqli_connect_errno()与MySQL 数据库建立连接时,发生错误时的错误编号。mysqli_connect_error()与MySQL 数据库建立连接时&#x…...
领域驱动设计:基于DDD的微服务设计实例
文章目录 项目基本信息战略设计战术设计后续的工作 用一个项目来了解 DDD 的战略设计和战术设计,走一遍从领域建模到微服务设计的全过程,一起掌握 DDD 的主要设计流程和关键 点。 项目基本信息 项目的目标是实现在线请假和考勤管理。功能描述如下&…...
【PB续命02】Oracle中加密及编码等
Oracle中实现Md5/Base64/AesBase64/UrlEncode等加密编码的使用备忘,参考其它人的贴子,Oracle 11g 亲测有效。 1. Oracle中实现Md5加密 SELECT lower(MD5(白龙马5217)) FROM dual; --返回结果 72853926982028ab8219921ad2918b8f --或 select utl_raw.…...
STM32-LTC6804方案成熟BMS方案
方案下载链接!!https://mp.weixin.qq.com/s?__bizMzU2OTc4ODA4OA&mid2247549092&idx1&snc73855c4e3d5afddd8608d8528864f95&chksmfcfb1373cb8c9a65a4bd1f545a1a587af882f209e7ccbb8944f4d2514d241ca1d7fcc4615e10&token539106225&a…...
一步一步认知机器学习
1,前言 之前学习并且实操了一些算法框架用来探索相关方向的可能性,但是总不了解相关的步骤。因为一步一步按照别人给出的步骤去操作,解决一些操作时出现的问题,基本可以达到目的。但是这个也基本限制了在那个框架而已。对于算法还…...
现代C++、STL、QTL的使用
0、现代C中最重要的是: 右值引用&&、移动语义std::move、完美转发std::forward、万能引用T&& void Func(int& x) { cout << "左值引用" << endl; } void Func(const int& x) { cout << "const左值引用…...
Android 备案公钥、签名 MD5获取方法
公钥和 MD5 值可以通过安卓开发工具、Keytool、Jadx-GUI 等多种工具获取,本文以 jadx-gui 为例。 1 windows 下载 jadx-gui 工具 下载 jadx-gui 工具 在这里选择一个下载 下载后 解压文件 双击运行程序,然后选择 release apk安装包 2 Mac 打开终端&a…...
1688拍立淘接口,按图搜索商品接口,图片识别接口,图片上传搜索接口,图片搜索API接口,以图搜货接口
1688拍立淘接口的作用是让用户通过上传图片或输入图片链接的方式,调用1688的图片搜索引擎,返回与该图片相关的所有1688商品。 使用该接口需要先获取一个key和secret,然后参考API文档里的接入方式和示例,查看测试工具是否有需要的…...
H3C AC通过Web平台进行AC软件的升级?
软件升级的流程 1、获取软件版本 登录新华三官网(首页>产品支持与服务>文档与软件>软件下载),将指定的软件版本下载至本地。 无线路由器-无线接入点-无线控制器-新华三集团-H3C 官网软件下载公共账号密码:账号&#x…...
网络通信和tcp协议
一、计算机网络架构模型 1、OSI七层模型 2、TCP/IP模型 3、TCP/IP协议族 无论是什么网络模型,都是为上一层提供服务,抽象层建立在低一层提供的服务上,每层都对应不同的协议 4、地址和端口号 1)MAC地址 MAC 地址共 48 位&#…...
React 核心与实战2023版
课程亮点: 完整的前后台项目(PC+移动;完成业务;)React 最新企业标准技术栈(React 18 + Redux + ReactRouter + AntD)React + TypeScript (为大型项目奠定了基础)课程内容安排: React 介绍 React 是什么? React 是由Meta公司研发,是一个用于 构建Web和原生交互界面…...
在 Android 上测试 Kotlin 协程
文章目录 官方文档在测试中调用挂起函数TestDispatchersStandardTestDispatcherUnconfinedTestDispatcher 注入测试调度程序设置主调度程序在测试之外创建调度程序创建您自己的 TestScope注入作用域 官方文档 https://developer.android.google.cn/kotlin/coroutines/test?hl…...
应用升级/灾备测试时使用guarantee 闪回点迅速回退
1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间, 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点,不需要开启数据库闪回。…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...
(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...
SpringCloudGateway 自定义局部过滤器
场景: 将所有请求转化为同一路径请求(方便穿网配置)在请求头内标识原来路径,然后在将请求分发给不同服务 AllToOneGatewayFilterFactory import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; impor…...
Yolov8 目标检测蒸馏学习记录
yolov8系列模型蒸馏基本流程,代码下载:这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中,**知识蒸馏(Knowledge Distillation)**被广泛应用,作为提升模型…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
Qt 事件处理中 return 的深入解析
Qt 事件处理中 return 的深入解析 在 Qt 事件处理中,return 语句的使用是另一个关键概念,它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别:不同层级的事件处理 方…...
热烈祝贺埃文科技正式加入可信数据空间发展联盟
2025年4月29日,在福州举办的第八届数字中国建设峰会“可信数据空间分论坛”上,可信数据空间发展联盟正式宣告成立。国家数据局党组书记、局长刘烈宏出席并致辞,强调该联盟是推进全国一体化数据市场建设的关键抓手。 郑州埃文科技有限公司&am…...
