对于Kotlin DSL的简单解析与使用

DSL(领域特定语言)是Kotlin所带来的强大语法特性之一,也是Java中所不存在的功能,JetBrain也基于DSL开发出了众多的开源库,Kotlin的开发者可以使用DSL来重构许多已有的代码,甚至有可能做到彻底抛弃HTML,XML,SQL等代码的地步。

简单介绍DSL

DSL是领域特定语言的英文缩写。那到底什么是领域特定语言?

我们最常使用的领域特定语言就是SQL以及正则表达式,SQL和正则表达式都只能解决它们特定领域内的问题,SQL用于数据库操作,而正则表达式则是用来处理文本字符串,它们也都有自己的语法,但是你无法使用它们在计算机上编写完整的程序;所以它们并不是我们常规意义上理解的"编程语言",那些有能力在计算机上编写几乎任何程序的编程语言,诸如,Kotlin,Java,Python等等我们有一个专业术语来定义它们,叫做图灵完备语言,而上面介绍的那些DSL就不是图灵完备的。

Kotlin DSL 的特点

Kotlin DSL 利用 Kotlin 语言的灵活性,允许我们创建领域特定语言。它可以让我们编写更简洁、更优雅的代码,同时提高代码可读性。

使用到的Kotlin语言特性

Kotlin DSL的例子

我们来举一个Kotlin DSL的例子,如果我们使用JetBrain构建Android UI的开源库Anko的话,我们可以用DSL重构一份XML代码;我们先来看看XML:

 <LinearLayout

 android:layout_width="match_parent"

 android:layout_height="match_parent"

 android:padding="30dp"

 android:orientation="vertical" >

 

 <EditText

 android:layout_width="wrap_content"

 android:layout_height="wrap_content"

 android:hint="Name"

 android:textSize="24sp" />

 

 <EditText

 android:layout_width="wrap_content"

 android:layout_height="wrap_content"

 android:hint="Password"

 android:textSize="24sp" />

 

 <Button

 android:layout_width="wrap_content"

 android:layout_height="wrap_content"

 android:text="Login"

 android:textSize="26sp" />

 

 </LinearLayout>

如果换成DSL来编写如下:

    lineatLayout {

        orientation = LinearLayout.VERTICAL

        padding = dip(30)

        editText {

            hint = "Name"

            textSize = 24f

        }.lparam(wrapContent, wrapContent)

        editText {

            hint = "Password"

            textSize = 24f

        }.lparam(wrapContent, wrapContent)

        button("Login") {

            textSize = 26f

        }.lparam(wrapContent, wrapContent)

    }

即使你不是Android开发者,也可以轻易的看出两者的异同,XML中的元素:LinearLayout, EditText,Button等的层级嵌套关系和DSL中的完全一至,属性的赋值也是应有尽有;这说明,如果你想把当前的一些编写起来不那么方便的代码,迁移到基于Kotlin DSL的库,大多数情况下其实学习成本并不高,实际上变化的只是一些简单的语法规则。 我们在开始下一节之前,先来看看这一段DSL代码,做一个简单的分析,并提出几个问题。我可以首先先告诉大家一个结论,linearLayout {},editText {},button {},这些东西全部都是Kotlin高阶函数,而orientation和padding这些都是Kotlin中的属性;学习过Kotlin的高阶函数的你应该知道,linearLayout {}大括号的内部实际上是一个lambda表达式,它作为一个参数,被传递给了函数linearLayout,而在这个lambda表达式的外部,你是无法引用到orientation和padding属性的,同理,在editText {}的lambda表达式的外部,也是无法引用到hint和textSize属性的。因为orientation是LinearLayout类的属性,而hint和textSize是EditText类的属性;这也就说明在这些lambda表达式的内部,持有了一个对这些类型对象的引用;而这样的lambda表达式就是带接收者的lambda。

深入理解带接收者的lambda

对象调用其对应的类内部的方法,是所有有面向对象编程经验的开发者都知道的原则,但这里要讲清楚带接收者的lambda,还是要从这里讲起。我们先来看下面的例子:

class A {

 fun function1() {

        function2()

    }

 

 fun function2() {

 // do something...

    }

}

 

// 扩展函数

fun A.function3() {

    function1()

    function2()

}

 

fun main(args: Array<String>) {

 val a = A()

    a.function1()

    a.function2()

    a.function3()

}

代码很基础,function1和function2都是A的成员函数,在function1中可以直接调用function2,即在同一个类的方法中可以直接调用另一个方法,而在A的外面,我们则需要创建一个A的对象来调用function1和function2;因为在A的内部,所有的成员(变量/函数)都持有一个A类型对象的引用,而在A的外部,在调用这些成员的时候,我们需要知道调用它的到底是哪一个对象,这是最基本的类和对象之间的关系,我就不再多说了。但在Kotlin中唯一的例外就是扩展函数,在扩展函数中调用其接收者的成员函数(或属性)可以直接调用,这是因为在A的外部调用它的扩展函数,需要一个A的对象。学过高阶函数和lambda编程后我们都知道,函数和lambda在很多时候可以认为是同一种东西,都可以把它们看作是一种有类型的(类型由参数类型,数量,顺序以及返回值类型来确定)可被执行,且可以被保存在一个变量中的代码段;所以带接收者的lambda在某些时候可以认为和扩展函数是等价的(注意,只是某些时候,因为lambda和函数在被编译成.class字节码以后是不同的,这是另一个话题,这里不再展开了),假如我们要定义一个A类型作为接收者类型且一个Int类型作为参数,无返回值的带接收者的lambda,就可以像如下这样定义:

val receiver: A.(Int) -> Until = {

 // do something...

}

如果我们要调用执行这个lambda:

val a = A()

a.receiver(3)

所以Part 1中介绍的那些诸如linearLayout {},editText {},button {}这些函数,都是以一个带接收者的lambda作为参数的普通内联函数,让我们以editText {}为例来看看它是如何定义的:

inline fun ViewManager.editText(init: (@AnkoViewDslMarker android.widget.EditText).() -> Unit): android.widget.EditText {

 return ankoView(`$$Anko$Factories$Sdk25View`.EDIT_TEXT, theme = 0) { init() }

}

 

inline fun <T : View> ViewManager.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {

 val ctx = AnkoInternals.wrapContextIfNeeded(AnkoInternals.getContext(this), theme)

 val view = factory(ctx)

    view.init()

    AnkoInternals.addView(this, view)

 return view

}

看起来有点复杂,启示拆开来看其实很简单,首先这是一个扩展函数,接收者是ViewManager,这样就限制了这个函数的调用范围,即只能在某个父布局中被调用,随后我们看到参数init就是一个标准的带接收者的lambda,而init在函数内部调用ankoView函数的时候又会在它的lambda参数中被调用,ankoView函数用来生成一个EditText对象,至于内部的原理,我们不去分析,而editText函数又会将这个EditText对象返回,便于函数的调用者获取这个对象的引用;最后我们看到,整个函数加了inline修饰符,即被声明成内联的,这样就保证了DSL API的执行效率,而执行init这个带接收者lambda的ankoView实际上也是ViewManager的扩展函数,而且它也是内联的,这里不再做过多的源码深入。我们简单的体验了一下如何声明一个DSL API,从Anko来看,实际上就是以下三点:

  • 1.使用扩展函数来限制函数的调用范围
  • 2.使用带接收者的lambda来保证API中的嵌套关系
  • 3.使用inline修饰符,把这些有lambda表达式作为参数的函数声明成内联的来保证执行效率 我们这里再详细说一下第二点。 我们在编写HTML和XML的时候,其中一点非常重要,那就是嵌套关系;这些嵌套关系即保证了这些元素之间的包含和被包含的关系,又保证了HTML或XML的可读性;以使用XML来编写Android UI为例,如果不使用XML,而是直接编写Java代码的话,也是可行的,但是我们只能使用Java那种从上到下不停new出一个对象,然后用对象不停调用不同方法的办法来创建UI,当然也是可行的,但是这几乎可以说是让代码的可读性瞬间归零,这样编写代码即容易出错,后期也几乎不可维护。但是现在Kotlin有了带接收者的lambda,我们可以在保留嵌套关系的同时,使用Kotlin这样的图灵完备语言来编写我们需要的UI,这样就实现了Part 1中提到的内部DSL的全部优点。

函数式的对象的invoke约定

Kotlin的约定有很多种,而比如使用便捷的get操作,以及重载运算符等等,invoke约定也仅仅是一种约定而已;我们可以把lambda表达式或者函数直接保存在一个变量中,然后就像执行函数一样直接执行这个变量,这样的变量通常声明的时候都被我们赋值了已经直接定义好的lambda,或者通过成员引用而获取到的函数;但是别忘了,在面向对象编程中,一个对象在通常情况下都有自己对应的类,那我们能不能定义一个类,然后通过构造方法来产生一个对象,然后直接执行它呢?这正是invoke约定发挥作用的地方。

class A(val str: String) {

 operator fun invoke() {

        println(str)

    }

}

 

fun main(args: Array<String>) {

 val a = A("Hello")

    a()

}

 

输出:Hello

我们只需要在一个类中使用operator来修饰invoke函数,这样的类的对象就可以直接像一个保存lambda表达式的变量一样直接调用,而调用后执行的函数就是invoke函数。 我们还有另一种方式来实现可调用的对象,即让类继承自函数类型,然后重写invoke方法:

class A : (String) -> String {

 override fun invoke(str: String): String {

        println(str)

 return str

    }

}

 

fun main(args: Array<String>) {

 val a = A("Hello")

    println(a())

}

输出:Hello

Hello

直接让一个类继承自函数类型,这样invoke的函数类型就和继承的类型一致了,我们也可以像上面那样直接调用A类的对象,最终会执行invoke函数。 使用invoke约定可以构建出什么样的DSL API呢?在Anko中好像还没有发现这样的例子,但是在Gradle的构建脚本中这样的例子就比较常见:

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

dependiences {

    compile("junit:junit:4.11")

}

dependiences实际上就是一个对象,它既可以直接调用compile方法,又能在它的lambda表达式参数内调用compile,可见dependiences也是一个使用了invoke约定的类的对象,而它接收的是一个带接收者的lambda表达式作为函数参数。 带接收者的lambda和invoke约定是支撑Kotlin DSL的两大语法特性,但实际上在Kotlin中众多的语法糖中,还有许多特性为你设计DSL的优雅语法提供了可能,这其中包括了:中辍调用,运算符重载,括号外的lambda等等等等;我们不妨充分发散自己的思维,让我们使用这些众多的优雅语法构建一个属于自己的DSL库,用来解决编程中某一类特定领域的棘手问题;Json数据格式也是一个讲究嵌套的数据格式,我们能否充分发挥我们的想象来编写一个基于DSL的库,来对Json做点什么呢?

那些优秀的DSL开源库

下面介绍的Kotlin DSL开源库都是Kotlin的亲爹JetBrain开发的,这说明,就目前来看广大开发者应该还没有把DSL的潜力发挥到极致,如果您有其它优秀的的DSL库推荐,可以给文章留言。

  • 数据库操作:Exposed Exposed是JetBrain推出的,可以使用DSL代替SQL来操作数据库的开源库,项目地址如下:Exposed
  • 动态构建Android UI:Anko Anko也是JetBrain推出的,上文已经提到过了;它是一款便于Android开发者使用Kotlin进行Android开发的函数库,其中,使用DSL动态构建Android UI只是其中的一部分功能,这个库的Github地址如下:Anko
  • 动态构建HTML布局:kotlinx.html 也是JetBrain官方推出的库,用来使用DSL来构建HTML布局,从它的包名中含有kotlinx就可以看出来,它的受重视程度高于Anko,基本上属于Kotlin官方develop kit中的一部分,它的Github地址如下: kotlinx.html 除此之外,Gradle已经支持使用Kotlin DSL来编写构建脚本,使用Gradle的同学,也不妨立刻开始尝试。

本文是对在Kotlin开发语言当中的DSL的一些原理和简单使用解析,对于修学Kotlin当然还有很多的进阶技术点。下面是一些总结,可以在主业点击可以看看详细的内容板块。

最后

Kotlin DSL 是一种强大的工具,可以帮助我们编写更简洁、优雅的代码。通过使用 Kotlin DSL,我们可以提高代码的可读性、灵活性和类型安全性。现在,让我们开始使用 Kotlin DSL,探索编程世界。

相关推荐
喵叔哟6 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生12 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
hopetomorrow26 分钟前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
小牛itbull36 分钟前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
请叫我欧皇i44 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
闲暇部落1 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
本当迷ya1 小时前
💖2025年不会Stream流被同事排挤了┭┮﹏┭┮(强烈建议实操)
后端·程序员
GIS瞧葩菜1 小时前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
chnming19871 小时前
STL关联式容器之set
开发语言·c++
熬夜学编程的小王1 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list