• 【Kotlin精简】第5章 简析DSL


    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'
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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'
        )
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    除了 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    要实现上面的语法糖,现在要做的第一件事就是创建一个新文件,将保持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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    所以在这里我们有一个创建一个Person对象的函数。它需要一个带有我们在第2行创建的对象的lambda。当在第3行执行这个lambda时,我们期望在返回第4行的对象之前,该对象获得它所需要的属性。下面展示如何使用这个函数:

    val person = person {
        it.name = "John"
        it.age = 25
    }
    
    • 1
    • 2
    • 3
    • 4

    由于这个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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    现在可以将其从DSL中删除:

    val person = person {
        name = "John"
        age = 25
    }
    
    • 1
    • 2
    • 3
    • 4

    到目前为止,还差一个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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    现在为Person添加一个地址函数,它接受一个Address作为接收者的lambda表达式,就像对person构造函数所做的那样。然后它将创建的Address对象设置为Person的属性:

    val person = person {
        name = "John"
        age = 25
        address {
            street = "Main Street"
            number = 42
            city = "London"
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.2 实现简单的UI布局

    我们先来看下这个布局

    
    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面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
                    }
               }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    首先要定义一种声明方式来初始化对象,所以可以写一个基于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
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

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

    context.FrameLayout {
           layout_width = match_parent
           layout_height = wrap_content
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而对于子控件,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) }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这样一个简单的动态布局就出来了,没想象中那么高级,其实就是对扩展函数高阶函数的运用。

    3.3 小结

    Kotlin DSL的好处,尤其是对View进行特定领域的处理的时候 很有用。

    • 有着近似 XML 的结构化表现力
    • 较少的字符串,更多的强类型,更安全
    • 可提取 linearLayoutParams 这样的对象方便复用
    • 在布局中同步嵌入 onClick 等事件处理
    • 如需要还可以嵌入 iffor 这样的控制语句

    4 DSL实现的原理

    4.1 扩展函数(扩展属性)

    package strings
    
    fun String.lastChar(): Char = this.get(this.length - 1)
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    带接收者的 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")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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 学习的东西是不一样的。

  • 相关阅读:
    分片上传与断点续传
    CUDA中Occupancy相关知识
    保温品牌不知道怎么选?中车的选择告诉你
    openssl编程-基础知识-OpenSSL简介
    v-if的使用
    PyCharm利用pydevd-pycharm实现Python远程调试
    将主键ID用括号包括 以字符串的方式进行存储 查询的技巧
    java-php-python-ssm糖果销售管理系统计算机毕业设计
    GPPT阅读笔记
    Python的基础语法(八)(持续更新)
  • 原文地址:https://blog.csdn.net/u010687761/article/details/133324441