Kotlin DSL
最近在看Kotlin,看到DSL部分觉得比较有意思,记录一下自己的理解
最简单的DSL就是各种大括号嵌套,每一个括号提供了一个独立的上下文,在该括号内使用这个上下文提供的api来影响上下文或者产生一些副作用。
Kotlin 天生就对DSL写法有比较好的支持,典型的scoping function: run、with、let、also、apply 就让大括号嵌套在Kotlin中无所不在
编写本文是因为看到了一段几十行的实现DSL的代码,觉得实现很简单,用法很炫酷,值得记录。代码链接在此 https://github.com/nazmulidris/color-console/blob/4d4503e183ab18434a9c86afd5ebdc66744c3451/src/main/kotlin/color_console_log/ColorConsoleLogUtils.kt#L91-L125
最开始错误的认知
不得不开始说一下我最开始的错误认知
1 | A { |
由于从其他语言的常规角度来说,读代码一般会有代码从上往下执行的思维定势,而且一开始看run, with, let, map, filter这些的时候会默认为以上代码是A执行完成之后得到中间结果,再将中间结果传递给xxx处理, 就是一个很直观的从上到下的执行过程。
其实这个地方是完全错误的理解,正确的理解是xxx代码块是整个A函数的一部分,即xxx在A函数内部执行,并不是A执行完成之后再执行xxx。心里有这个想法十分重要,这是DSL得以实现的基石
其实这种套路刚好和python的装饰器@
相反,装饰器是在函数的开始和结束之后执行,而kotlin的这个语法糖是在函数中间执行
另外一个和以往经验不同的是,其他语言所说的lambda基本是表示单个表达式,而kotlin中的lambda却表示多个表达式组合而成的代码块
先看看用法
看看那个几十行代码实现的DSL的用法
1 | colorConsole {//this: ColorConsoleContext |
可以看到主要特色就是有很多的大括号,而且注释也说明了,在每个大括号里面提供了什么样的上下文
最开始作为一个使用者,我们应该精确的理解DSL如何应用,毕竟并不是任意的嵌套都是有效的,其实防止误用很简单
- 每一个方法都提供了一个上下文,在这个上下文中应用上下文提供的方法和值。比如
printLine
提供了MutableList<String>
上下文。这个上下文提供了span
,add
等方法,我们应该使用它提供的方法对上下文造成影响,或者对环境造成副作用(比如打印,发送网络请求等) - 第一步提供了上下文,然后你在
{}
内部对上下文造成了影响,最后就是处理上下文。比如printLine
的处理就是打印输出上下文
因此,会知道以下嵌套虽然语法不报错,但是会和你预料的不同
1 | colorConsole {//this: ColorConsoleContext |
因为line
不是MutableList<String>
提供的方法,没有影响printLine提供的上下文,虽然line
产生了一个值,但是并没有输出处理
实现
应该也有复杂的,但是这个示例里面的实现是相当简单
首先单独看printLine
, 从使用倒推实现,即它是一个方法,最后一个参数是MutableList<String>.() -> Unit
. 在方法的前半部分生成一个``MutableList
1 | fun line(spanSeparator: String = ", ", |
后面提供了一些MutableList<String>
的方法,以供在上下文中处理
1 | fun MutableList<String>.span(color: Colors, text: String): MutableList<String> { |
入口处提供了一个静态方法配合apply,这样在整个上下文中就能使用printLine
和 line
了
1 | companion object { |
总结
为什么Kotlin能这样写DSL?
因为kotlin中如果方法的最后一个参数是函数,且该函数只有一个入参,调用的时候允许将最后一个参数放到后面,使用花括号
因为kotlin中lambda是代码块,而不只是单条表达式
以上两条造成的结果就是Kotlin 这样写没有什么违和感,其他语言不行