Kotlin DSL 实现原理介绍
DSL(领域建模语言),一种提供给用户的,比较简单、方便地实现某种功能的语言。在很多场景下都会使用到。笔者当前主要从事的,静态代码分析相关的工作,也会通过 DSL 简化规则的定制难度,方便扩展工具的能力。因此有对各种不同的 DSL 实现方式进行初步调研。本文主要介绍 Kotlin DSL 实现原理,后续会有一个文章,专门介绍部分基于 Kotlin DSL 的实现。
下面主要介绍几种 Kotlin DSL 实现的基础内容,方便进行扩展,不一定全面,但是相对于笔者要进行的静态代码分析工具的开发,足够了。
1. 中缀表达式
1.1 中缀表达式概念
中缀表达式就是将函数调用,写得和操作符使用一样,更符合大家的一般的阅读习惯。不再是纯粹的面向对象的思维。例如下面的代码:
改写成中缀表达式就是:
1.2 中缀表达式实现
在 Kotlin 中,定义中缀表达式需要在 fun 关键字前面加上 infix 关键字,并且根据上面的描述,infix 将函数调用写成中缀形式:
如上所示,我们在定义 plus 时,增加了一个 infix 关键字,在下面调用时,可以写成 4 plus 5,和我们的自然语言就很像,比上面的函数调用来的更自然。
通过上面的例子,我们可以梳理 中缀表达式 定义,需要的约束:
1) 必须是一个成员函数或者扩展函数(参考 1.1 中对应的表达式的使用场景);
2) 必须包含,且仅有一个参数,并且参数不能是默认参数或者可变参数。
2. 扩展函数和属性
2.1 扩展函数和属性实现
大部分情况下,我们都会基于一些基础库开发,而这些基础库可能缺少一些咱们需要的方法或者属性,不方便使用,或者运算,此时,可以使用扩展函数和属性的方法来定义。
首先,我们来看下面的例子:
上面的类 Person,很可能是第三方类库的内容,我们无法修改,但是,此时我们有下面的需求:
1) 拿到 Person 的 全名(firstName + " " + secondName);
2) 根据 Person 的 age 判断是 年轻人 还是 老年人。
如果放在以前(比如 Java 时代),我们需要创建 工具类,返回上面的信息,如下:
但是,有了扩展函数和属性后,就不需要再额外创建一个工具类了,如下:
仔细品品上面,在 Person 中增加扩展函数 和 扩展属性 后,在执行函数调用时的方式,跟内置的属性和函数一样。
2.2 扩展函数和属性约束和应用场景
如前面介绍,扩展函数和属性,一般应用于非自研代码(依赖的第三方代码或者基础库)的扩展,因此,扩展的时候,还需要遵循一定的约束:
1) 扩展的属性,无法凭空新增数据信息,及扩展属性的时候,get 方法获取的信息,也是从当前类中已有的其他信息中提取出来的;
2) 扩展的函数,不能覆盖已有的类的函数信息。
3. invoke 函数调用
invoke()方法是 kotlin 对象类中默认持有的方法,可以通过 operator 关键字重载 invoke()方法。
如下,是一个基本的 invoke 的例子:
上面,重写了 invoke 的方法,下面举一个新的例子:
如上,我们在第 3 行 invoke 方法中,传入了一个 lambda 表达式(会在下一小节进行介绍),然后在第 9 行中,传入了一个校验,认为一个 person 年龄应该在 0 到 150 之间(包含头尾),否则就不是一个合法的年龄,此时,第 9 行的 语句,已经有了一些 DSL 的味道了。
4. Lambda 表达式
lambda 的确是 Kotlin 中,非常牛叉的一个特性,而且用好了,可以给我们使用很大的便利。下面介绍几种简单的使用方式。我也不是特别有能力把 Lambda 表达式介绍的非常清楚,所以这里主要通过几个例子,介绍 Lambda 表达式的使用。
4.1 Lambda 表达式举例
可以看下面的代码:
我们可以看到,定义了三个 sum,都是两数求和的实现。其中:
1) 第一行,就是一个基本的函数定义;
2) 我们知道,Kotlin 中,val <variable>: <type> 是变量定义的基本方式,我们可以知道:(Int, Int) -> Int 是一个类型,表示是一个 Lambda 表达式,其中有两个参数,都是 Int,-> 后面,也有个 Int,表示有个返回值,也是 Int 类型;{a , b -> a + b} 前面已经有标注类型,因此这里不再需要类型。
3) {a : Int , b : Int -> a + b} 是 Lambda 表达式的基本的定义方式。
4.2 Lambda 表达式作为函数参数
从 4.1 节中,我们知道 Lambda 表达式的类型形如:(Int, Int) -> Int ,如果没有返回值,则可以表示为 Unit,因为是个类型,可以作为函数参数类型传递:
如上,test 函数的最后一个参数,就是 Lambda 表达式。
更重要的一点儿:如果 Lambda 表达式是函数的最后一个参数,则可以放到括号外面,则上面的代码,可以写成下面形式:
另外,如果 Lambda 表达式是函数的唯一参数时,括号都可以省略,如下:
请仔细观察 10-12 行的变化,第 10 行是普通的 Lambda 表达式作为入参的 函数调用,第 11 行,因为 Lambda 表达式是最后的一个参数,因此可以放到括号外,第 12 行,因为 Lambda 表达式是唯一参数,因此括号也可以省略。
4.3 带有接收者的 Lambda 表达式
其实在 4.2 节中,最后的一个例子,我们已经使用了带接收者的参数了,如下:
如上:Person.() -> Boolean,带接收者,其实就是告诉 Lambda 表达式,入参是个 Person 类型,并且可以直接使用相应的属性。
我们考虑不使用带有接收者的 Lambda 表达式实现:
如上,可以直观地看到两者的区别,使用带接收者的 Lambda 表达式会更加简单。
5. 总结
本文简单介绍了几种 Kotlin 中 DSL 定义时,用到的一些基本的方法。Kotlin DSL 在上面的基本方法的加持下,其实已经可以做到非常简单,绝对是可以做到开发一种 Kotlin DSL 实现,即使不懂 Java 或者 Kotlin 的同学,也可以尽快上手,完成实现。
随后会给出实现一个基于 Kotlin DSL 的一个例子,说明 Kotlin DSL 的简洁、强大的地方。
版权声明: 本文为 InfoQ 作者【maijun】的原创文章。
原文链接:【http://xie.infoq.cn/article/c80a085e1546334460964eb04】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论