【Kotlin精简】第5章 简析DSL

news2024/12/27 11:28:34

1 DSL是什么?

Kotlin 是一门对 DSL 友好的语言,它的许多语法特性有助于 DSL 的打造,提升特定场景下代码的可读性和安全性。本文将带你了解 Kotlin DSL 的一般实现步骤,以及如何通过 @DslMarkerContext Receivers 等特性提升 DSL 的易用性。

DSL 全称是 Domain Specific Language,即领域特定语言。顾名思义 DSL 是用来专门解决某一特定问题的语言,比如我们常见的 SQL 或者正则表达式等,DSL 没有通用编程语言(Java、Kotlin等)那么万能,但是在特定问题的解决上更高效。

2 Gradle Kotlin DSL的优点和使用

Gradle Kotlin DSLGradle 5.0引入的一种新型的Gradle脚本语言,作为Groovy语言的替代方案。
官方文档中提到,Kotlin DSL具有如下的优点:

  1. 类型安全:编写Gradle脚本时,可以进行静态类型检查,这样可以保证更高的代码质量和更好的可维护性;
  2. 代码提示:Kotlin语言具有良好的编码体验,比如IDE可以提示代码补全、语法错误等,这些在Groovy语言中不易得到;
  3. 使用简单:Kotlin是一种现代化的语言,语法易懂,学习成本低;
  4. 高效性:Gradle使用Kotlin编写的DSL脚本会比同样的Groovy脚本快2~10倍。

创作一套全新新语言的成本很高,所以很多时候我们可以基于已有的通用编程语言打造自己的 DSL,比如日常开发中我们将常见到 gradle 脚本 ,其本质就是来自 Groovy 的一套 DSL

android {
  compileSdkVersion 28
  defaultConfig {
    applicationId "com.my.app"
    minSdkVersion 24
    targetSdkVersion 30
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles 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'
    )
)

除了 GroovyKotlin 也非常适合 DSL 的书写,正因如此 Gradle 开始推荐使用 kts 替代 gradle,其实就是利用了 Kotlin 优秀的 DSL 特性。

3 Kotlin DSL 及其优势

KotlinAndroid 的主要编程语言,因此我们可以在 Android 开发中发挥其 DSL 优势,提升特定场景下的开发效率。例如 ComposeUI 代码就是一个很好的示范,它借助 DSLKotlin 代码具有了不输于 XML 的表现力,同时还兼顾了类型安全,提升了 UI 开发效率。

3.1 一个简单DSL例子

Kotlin中实现DSL构建要依靠这几样东西:

  1. 扩展函数;
  2. 带接收者的 Lambda 表达式;
  3. 在方法括号外使用Lambda

我们先来看一下一个DSL例子:

val person = person {
    name = "John"
    age = 25
    address {
        street = "Main Street"
        number = 42
        city = "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 = 25
    address {
        street = "Main Street"
        number = 42
        city = "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">

    <TextView
        android: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_parent
                layout_height = wrap_content

                TextView {
                    layout_id = "tv"
                    layout_width = match_parent
                    layout_height = match_parent
                    textSize = 16f
                    padding_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: Number
    get() {
        return 0
    }
    set(value) {
        val w = if (value.dp > 0) value.dp else value.toInt()
        val h = layoutParams?.height ?: 0
        updateLayoutParams<ViewGroup.LayoutParams> {
            width = w
            height = h
        }
    }
    
inline var View.layout_height: Number
    get() {
        return 0
    }
    set(value) {
        val w = layoutParams?.width ?: 0
        val h = if (value.dp > 0) value.dp else value.toInt()
        updateLayoutParams<ViewGroup.LayoutParams> {
            width = w
            height = h
        }
    }

这里的init就是上面说的带接受者的lamba表达式拉,所以代码里去实现一个FrameLayout布局就可以这样子拉

context.FrameLayout {
       layout_width = match_parent
       layout_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 等事件处理
  • 如需要还可以嵌入 iffor 这样的控制语句

4 DSL实现的原理

4.1 扩展函数(扩展属性)

package strings

fun 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,我们可以提高代码的可读性、灵活性和类型安全性。当然 AndroidDSL 远不止这些使用场景 ,但是实现思路都是相近的,最后再来一起回顾一下:

  1. DSL 是什么?
    DSL 是一种针对特殊编程场景的语言或范式,它处理效率更高,且表达式更为专业。
    例如 SQL、HTML、正则表达式等。
  2. Kotlin 如何支持 DSL
    通过 扩展函数、带接收者的函数类型等来支持使用 DSL。
  3. Kotlin 自定义 DSL 的优势
    提供一套编程风格,可以简化构建一些复杂对象的代码,提高简洁程度的同时,具备很高的可读性。
  4. Kotlin 自定义 DSL 的缺点
    构造代码较为复杂,有一定上手难度,非必要不使用。

Tips: 对于顶级的Android发烧友,或者是Kotlin学习爱好者可以深度去挖掘DSL,或者是高级的Kotlin语法糖。注意对于在职场打拼的各位朋友们,还是那句话:学值得变现的知识点,并且要等机会来变现,从这个角度,Kotlin会用就可以了,不一定要非要死磕语法糖。切记。职场和自由职业free style 学习的东西是不一样的。

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

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

相关文章

音乐制作软件 Studio One 6 mac中文版软件特点

Studio One mac是一款专业的音乐制作软件&#xff0c;该软件提供了全面的音频编辑和混音功能&#xff0c;包括录制、编曲、合成、采样等多种工具&#xff0c;可用于制作各种类型的音乐&#xff0c;如流行音乐、电子音乐、摇滚乐等。 Studio One mac软件特点 1. 直观易用的界面&…

Spring中静态代理设计模式

目录 一、为什么需要代理设计模式 二、代理设计模式 三、静态代理设计模式 3.1 存在的问题 一、为什么需要代理设计模式 在项目的开发过程中我们知道service层是整个项目中最重要的部分&#xff0c;在service中一般会有两个部分&#xff0c;一个是核心业务&#xff0c;一个是额…

DJYROS产品:基于DJYOS的国产自主割草机器人解决方案

基于都江堰泛计算操作系统的国产自主机器人操作系统即将发布…… 1、都江堰机器人操作系统命名&#xff1a;DJYROS 2、机器人算法&#xff1a;联合行业自主机器人厂家&#xff0c;构建机器人算法库。 3、机器人芯片&#xff1a;联合行业机器人AI芯片公司&#xff0c;构建专用…

电商API是何时?以什么姿态开启了它的时代?

说到API&#xff0c;非业内技术人士&#xff0c;大家似乎对它还是知之甚少。 但如果有关注这个领域&#xff0c;其实不难发现&#xff0c;国内一些所谓大厂已经在电商API接口方面做了不少动作&#xff0c;不论是对外宣称的API生态&#xff0c;还是相对低调的API市场&#xff0c…

冲刺学习-MySQL-基础

基础 数据类型 常见数据类型的属性 整型 TINYINT、SMALLINT、MEDIUMINT、INT&#xff08;INTEGER&#xff09;和 BIGINT 可选属性 M&#xff1a;表示显示宽度&#xff08;从MySQL 8.0.17开始&#xff0c;整数数据类型不推荐使用显示宽度属性&#xff09;UNSIGNED&#xff1…

hdlbits系列verilog解答(7458芯片)-10

文章目录 wire线网类型介绍一、问题描述二、verilog源码三、仿真结果 wire线网类型介绍 wire线网类型是verilog的一种数据类型&#xff0c;它是一种单向的物理连线。它可以是输入也可以是输出&#xff0c;它与reg寄存器数据类型不同&#xff0c;它不能存储数据&#xff0c;只能…

设计模式——七大原则详解

这里写目录标题 设计模式单一职责原则应用实例注意事项和细节 接口隔离原则应用实例 依赖倒转&#xff08;倒置&#xff09;原则基本介绍实例代码依赖关系传递的三种方式注意事项和细节 里氏替换原则基本介绍实例代码 开闭原则基本介绍实例代码 迪米特法则基本介绍实例代码注意…

【苍穹外卖 | 项目日记】第八天

前言&#xff1a; 昨天晚上跑完步回来宿舍都快停电了&#xff0c;就没写项目日记&#xff0c;今天补上 目录 前言&#xff1a; 今日完结任务&#xff1a; 今日收获&#xff1a; 引入百度地图接口&#xff1a; 引入spring task &#xff0c;定时处理异常订单&#xff1a; …

ssm+vue的软考系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的软考系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff…

【RNA structures】RNA-seq Part2: RNA转录的重构和前沿测序技术

文章目录 RNA转录重建1 先简单介绍一下测序相关技术2 Map to Genome Methods2.1 Step1 Mapping reads to the genome2.2 Step2 Deal with spliced reads2.3 Step 3 Resolve individual transcripts and their expression levels 3 Align-de-novo approaches3.1 Step 1: Generat…

你还不会DeBug?太low了吧

编程时调试是不可缺少的&#xff0c;Unity中用于调试的方法均在Debug类中。 浅试一下 新建一个物体和脚本&#xff0c;并把脚本挂载到物体上&#xff01; using System.Collections; using System.Collections.Generic; using UnityEngine;public class DeBugTest : MonoBeh…

JavaSE入门---掌握面向对象三大特性:封装、继承和多态

文章目录 封装什么是封装&#xff1f;如何实现封装&#xff1f; 继承什么是继承&#xff1f;继承的语法父类成员访问子类访问父类的成员变量子类访问父类的成员方法 认识super关键字认识final关键字子类的构造方法super VS this在继承关系中各代码块的执行顺序是怎样的&#xf…

AAPCS:最新的ARM子程序调用规则

AAPCS是arm公司发布的ARM架构应用程序二进制&#xff08;ABI&#xff09;程序调用接口&#xff0c;该文档由多个版本&#xff0c;博主第一次ARM程序调用规则是在《ARM体系与结构编程》&#xff0c;但书中描述的是ATPCS&#xff0c;AAPCS是ATPCS的升级版。后面去ARM官网看到了AA…

冯诺依曼体结构 - 为什么要有操作系统

冯诺依曼体系结构 基础 概念认识 我们现在常见的 计算机&#xff0c;如 笔记本电脑&#xff0c;或者是不常见的 计算机&#xff0c;如服务器&#xff0c;大部分都遵循一个体系结构 -- 冯诺依曼体系结构。计算机的基本构成 就是由 冯诺依曼体系结构 来构成计算机的基本单元的。…

机械设备经营小程序商城的作用是什么

由于机械设备厂商品牌需要各地招商代理&#xff0c;因此在管理方面也需要工具进行高效管理。如今各个行业都在开展数字化转型解决行业所遇难题或通过线上销售解决传统三公里难题及品牌扩张难题、用户消费渠道少等难题&#xff0c;构建会员体系精细化管理&#xff0c;同时还需要…

轻松快速搭建一个本地的语音合成服务

前言 本文将介绍一个可以快速搭建本地语音合成的服务&#xff0c;模型和代码已全部提供&#xff0c;不需要联网运行。项目使用的是VITS模型结构&#xff0c;能够很轻松地启动服务。 安装环境 安装Pytorch。 # 安装CPU版本的Pytorch conda install pytorch torchvision torc…

【了解一下,Elastic Search的检索】

文章目录 **1.1.ES**1.1.1.elasticsearch的作用**1.1.2.ELK栈****2.索引库操作****2.1.mapping映射属性****2.2.索引库的CRUD** **3. 文档操作** **基于IDEA操作ES****索引操作****文档操作** DSL查询文档**1.1.DSL查询分类****1.2. 全文检索查询****1.3. 精准查询****1.4. 地理…

消息订阅与发布pubsub

消息订阅与发布 订阅消息&#xff1a;消息名发布消息&#xff1a;消息内容 A组件想使用C组件里面的东西&#xff0c;A需要数据&#xff0c;C是提供者。 在A组件里面订阅一个消息&#xff0c;假设订阅的消息名叫做Demo&#xff0c;指定回调函数叫test&#xff1b;C发布消息&…

ARM映像文件组成

引言 ARM编译器将各种源文件&#xff08;汇编文件、C语言程序文件、C语言程序文件&#xff09;编译生成ELF格式的目标文件&#xff08;后缀为.o文件&#xff0c;以下将目标文件简称为.o文件&#xff09;&#xff0c;.o文件经过连接器&#xff0c;和C/C运行时库一起编译生成ELF格…

Kafka学习(最新版3.6.0)

文章目录 一、初识MQ1.1 什么是MQ1.2 同步和异步通讯1.1.1 同步通讯1.1.2 异步通讯 1.3 技术对比1.4 MQ的两种模式 二、初识Kafka2.1 Kafka的使用场景2.2 Kafka基本概念2.3 Topic与Partition 三、Kafka基本使用3.1 部署前的准备3.2 启动kafka服务器3.3 Kafka核心概念之Topic3.4…