写点什么

Kotlin DSL 实现原理介绍

用户头像
maijun
关注
发布于: 2 小时前

DSL(领域建模语言),一种提供给用户的,比较简单、方便地实现某种功能的语言。在很多场景下都会使用到。笔者当前主要从事的,静态代码分析相关的工作,也会通过 DSL 简化规则的定制难度,方便扩展工具的能力。因此有对各种不同的 DSL 实现方式进行初步调研。本文主要介绍 Kotlin DSL 实现原理,后续会有一个文章,专门介绍部分基于 Kotlin DSL 的实现。


下面主要介绍几种 Kotlin DSL 实现的基础内容,方便进行扩展,不一定全面,但是相对于笔者要进行的静态代码分析工具的开发,足够了。

1. 中缀表达式

1.1 中缀表达式概念

中缀表达式就是将函数调用,写得和操作符使用一样,更符合大家的一般的阅读习惯。不再是纯粹的面向对象的思维。例如下面的代码:

obj1.function(obj2)
复制代码

改写成中缀表达式就是:

obj1 function obj2
复制代码

1.2 中缀表达式实现

在 Kotlin 中,定义中缀表达式需要在 fun 关键字前面加上 infix 关键字,并且根据上面的描述,infix 将函数调用写成中缀形式:

infix fun Int.plus(that: Int): Int {return this + that}
fun main() { // val x = 4.plus(5) val x = 4 plus 5 println(x)}
复制代码

如上所示,我们在定义 plus 时,增加了一个 infix 关键字,在下面调用时,可以写成 4 plus 5,和我们的自然语言就很像,比上面的函数调用来的更自然。

通过上面的例子,我们可以梳理 中缀表达式 定义,需要的约束:

1) 必须是一个成员函数或者扩展函数(参考 1.1 中对应的表达式的使用场景);

2) 必须包含,且仅有一个参数,并且参数不能是默认参数或者可变参数。

2. 扩展函数和属性

2.1 扩展函数和属性实现

大部分情况下,我们都会基于一些基础库开发,而这些基础库可能缺少一些咱们需要的方法或者属性,不方便使用,或者运算,此时,可以使用扩展函数和属性的方法来定义。

首先,我们来看下面的例子:

data class Person(val firstName: String, val lastName: String, val age: Int)
复制代码

上面的类 Person,很可能是第三方类库的内容,我们无法修改,但是,此时我们有下面的需求:

1) 拿到 Person 的 全名(firstName + " " + secondName);

2) 根据 Person 的 age 判断是 年轻人 还是 老年人。

如果放在以前(比如 Java 时代),我们需要创建 工具类,返回上面的信息,如下:

object PersonUtil {  fun getFullName(person: Person) = person.firstName + " " + person.lastName  fun getStage(person: Person) = if (person.age > 60) "old" else "young"}
fun main() { val person = Person("zhang", "san", 54) println(PersonUtil.getFullName(person)) println(PersonUtil.getStage(person))}
复制代码

但是,有了扩展函数和属性后,就不需要再额外创建一个工具类了,如下:

val Person.fullName: Stringget() = this.firstName + " " + this.lastName
fun Person.getStage(): String = if (this.age > 60) "old" else "young"
fun main() { val person = Person("zhang", "san", 54) println(person.fullName) println(person.getStage())}
复制代码

仔细品品上面,在 Person 中增加扩展函数 和 扩展属性 后,在执行函数调用时的方式,跟内置的属性和函数一样。

2.2 扩展函数和属性约束和应用场景

如前面介绍,扩展函数和属性,一般应用于非自研代码(依赖的第三方代码或者基础库)的扩展,因此,扩展的时候,还需要遵循一定的约束:

1) 扩展的属性,无法凭空新增数据信息,及扩展属性的时候,get 方法获取的信息,也是从当前类中已有的其他信息中提取出来的;

2) 扩展的函数,不能覆盖已有的类的函数信息。

3. invoke 函数调用

invoke()方法是 kotlin 对象类中默认持有的方法,可以通过 operator 关键字重载 invoke()方法。

如下,是一个基本的 invoke 的例子:

data class Person(val firstName: String, val lastName: String, val age: Int)
operator fun Person.invoke() { println(this.firstName + " " + this.lastName)}
fun main() { val person = Person("zhang", "san", 54) person()}
复制代码

上面,重写了 invoke 的方法,下面举一个新的例子:

data class Person(val firstName: String, val lastName: String, val age: Int)
operator fun Person.invoke(block: Person.() -> Boolean): Boolean { return block(this)}
fun main() { val person = Person("zhang", "san", 160) val checked = person { (age >= 0) and (age <= 150) } println(checked)}
复制代码

如上,我们在第 3 行 invoke 方法中,传入了一个 lambda 表达式(会在下一小节进行介绍),然后在第 9 行中,传入了一个校验,认为一个 person 年龄应该在 0 到 150 之间(包含头尾),否则就不是一个合法的年龄,此时,第 9 行的 语句,已经有了一些 DSL 的味道了。

4. Lambda 表达式

lambda 的确是 Kotlin 中,非常牛叉的一个特性,而且用好了,可以给我们使用很大的便利。下面介绍几种简单的使用方式。我也不是特别有能力把 Lambda 表达式介绍的非常清楚,所以这里主要通过几个例子,介绍 Lambda 表达式的使用。

4.1 Lambda 表达式举例

可以看下面的代码:

fun sum1(x: Int, y: Int) = x + yval sum2 : (Int , Int) -> Int = {a , b -> a + b}val sum3 = {a : Int , b : Int -> a + b}
fun main() { println(sum1(3, 4)) // 7 println(sum2(3, 4)) // 7 println(sum3(3, 4)) // 7}
复制代码

我们可以看到,定义了三个 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,因为是个类型,可以作为函数参数类型传递:

fun test(x: Int, y: Int, op: (Int, Int) -> Int): Int {return op(x, y)}
fun main() { val result = test(3, 4, {x:Int, y:Int -> x+y}) println(result)}
复制代码

如上,test 函数的最后一个参数,就是 Lambda 表达式。

更重要的一点儿:如果 Lambda 表达式是函数的最后一个参数,则可以放到括号外面,则上面的代码,可以写成下面形式:

fun test(x: Int, y: Int, op: (Int, Int) -> Int): Int {  return op(x, y)}
fun main() { val result = test(3, 4) { x: Int, y: Int -> x + y } println(result)}
复制代码

另外,如果 Lambda 表达式是函数的唯一参数时,括号都可以省略,如下:

data class Person(val firstName: String, val lastName: String, val age: Int)
fun Person.check(block: Person.() -> Boolean): Boolean { return block(this)}
fun main() { val person = Person("zhang", "san", 160) val checked1 = person.check({ (age >= 0) and (age <= 150) }) val checked2 = person.check(){ (age >= 0) and (age <= 150) } val checked3 = person.check { (age >= 0) and (age <= 150) }
println(checked1) // false println(checked2) // false println(checked3) // false}
复制代码

请仔细观察 10-12 行的变化,第 10 行是普通的 Lambda 表达式作为入参的 函数调用,第 11 行,因为 Lambda 表达式是最后的一个参数,因此可以放到括号外,第 12 行,因为 Lambda 表达式是唯一参数,因此括号也可以省略。

4.3 带有接收者的 Lambda 表达式

其实在 4.2 节中,最后的一个例子,我们已经使用了带接收者的参数了,如下:

fun Person.check(block: Person.() -> Boolean): Boolean {return block(this)}
复制代码

如上:Person.() -> Boolean,带接收者,其实就是告诉 Lambda 表达式,入参是个 Person 类型,并且可以直接使用相应的属性。

我们考虑不使用带有接收者的 Lambda 表达式实现:

data class Person(val firstName: String, val lastName: String, val age: Int)
fun Person.check(block: (person: Person) -> Boolean): Boolean { return block(this)}
fun main() { val person = Person("zhang", "san", 160) val checked1 = person.check({ p -> (p.age >= 0) and (p.age <= 150) }) val checked2 = person.check(){ p -> (p.age >= 0) and (p.age <= 150) } val checked3 = person.check { p -> (p.age >= 0) and (p.age <= 150) }
println(checked1) // false println(checked2) // false println(checked3) // false}
复制代码

如上,可以直观地看到两者的区别,使用带接收者的 Lambda 表达式会更加简单。

5. 总结

本文简单介绍了几种 Kotlin 中 DSL 定义时,用到的一些基本的方法。Kotlin DSL 在上面的基本方法的加持下,其实已经可以做到非常简单,绝对是可以做到开发一种 Kotlin DSL 实现,即使不懂 Java 或者 Kotlin 的同学,也可以尽快上手,完成实现。

随后会给出实现一个基于 Kotlin DSL 的一个例子,说明 Kotlin DSL 的简洁、强大的地方。

发布于: 2 小时前阅读数: 2
用户头像

maijun

关注

还未添加个人签名 2019.09.20 加入

还未添加个人简介

评论

发布
暂无评论
Kotlin DSL实现原理介绍