天天看点

Kotlin DSL构建

Kotlin对整洁语法的支持

常规语法 整洁语法 用到的功能
StringUtil.capitalize(s) s.capitalize() 扩展函数
1.to(“one”) 1 to “one” 中缀调用
set.add(2) set+=1 运算符重载
map.get(“key”) map[“key”] get方法约定
file.use({f->f.read() } ) file.use{it.read()} 括号外的lambda
sb.append(“yes”) sb.append(“no”) with(sb){ append(“yes”) \n append(“no”)} 带接收者的lambda

DSL语言分类及特点

通用编程语言: 有一系列足够完善的能力来解决几乎所有能被计算机解决的问题

领域特定语言:专注在特定的任务或者领域上,并放弃的与该领域无关的功能 (外部DSL),而领域特定语言分为外部DSL与内部DSL

DSL更趋向声明式 :语言包括有命令式和声明式写法 ,命令式语言描述执行操作所需步骤的确切序列,每个操作实现都被独立化了,而声明式描述了想要的结果并将执行细节留给解释它的引擎,通常让执行更有效率。

外部DSL语言:

声明式写法,很难与通用编程语言的宿主应用程序结合起来使用,外部DSL语言自己的语法并不能直接嵌套使用。

内部DSL: 是使用通用编程语言编写程序的一部分,包含了外部DSL声明式和通用语言的语法优点

DSL 的结构

DSL与普通的api之间并没有明确的边界,但是DSL经常会出现一个通常在其他api中不存在的特征:结构或文法

结构:api的前后调用在一个大的结构块中。中间需要维护调用的上下文信息

命令式api:前后调用没有内在的结构,也不需要维护上下文。

比如常见的DSL结构文法:

android{

  sourceSets {
      main {
          java {
              srcDir 'src/java' // 指定源码目录
          }
          resources {
              srcDir 'src/resources' //资源目录
          }
      }
  }

  defaultconfig{
  
  }

}      
构建结构化的API:DSL中带接收者的lambda

先来看一个表达式里面的知识点

/**
 * 1.高阶函数
 * 2.block: () -> Unit  Lambda表达式
 * 3.T类型的扩展函数
 * 4.泛型函数
 * 5.带接收者的Lambda block: T.() -> Unit
 */
inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}      
普通函数类型如何转换成扩展函数类型
  1. 函数参数类型 = 函数类型(builderAction:( StringBuilder)-> Unit),将函数类型转换为扩展

    函数类型

  2. 将函数类型签名中的一个参数(类型)移到括号前面,并用一个.将它与其他的( 参

    数)类型分隔开。用 StringBuilder.() -> Unit 代替(StringBuilder ) -> Unit

  3. 这个特殊的类型( StringBuilder )就叫作接收者类型,传递给 lambda 的这个类型的值就叫作接收者对象
fun buildString( builderAction:( StringBuilder)-> Unit ): String { 
  val sb = StringBuilder ()
  sb.builderAction()
  return sb . toString ()
}      

函数类型转换成扩展函数类型

fun buildString( builderAction:StringBuilder.()-> Unit ): String { 
  val sb = StringBuilder ()
  sb.builderAction()
  return sb . toString ()
}      

扩展函数类型表达式

String.(Int,Int)->Unit

一个扩展函数类型,接收者类型是 String,两个参数类型是 Int ,返回类型是 Unit

当你将一个普通函数类型转换为扩展函数类型时,其调用方式也发生了变化 。 像调用一个扩展函数那样调用 lambda,而不是将对象作为参数传递给 lambda。在 使用普通 lambda 时,我们使用这样的语法将一个 StringBuilder 实例作为 参 数给它 :builderActi on (sb )。但当 你将它改成带接收者的 lambda 时,代码 就变成了 sb .builderAction () 。再次重 申,这里的 builderAction 并不是 StringBuilder 类的方法,它是一个函数类型的参数,但可以用调用扩展函数一 样的语法调用它。
使用 invoke约定构建更灵活的代码块嵌套

invoke约定允许把自定义类型的对象当做函数一样调用。(函数类型对象可以作为函数调用),定义invoke需要使用operator 修饰符进行修饰。入参和返回类型可以是任意受系统支持的类型:

//源自 Functions.kt
public interface Function2<in P1, in P2, out R> : Function<R> {
    /** Invokes the function with the specified arguments. */
    public operator fun invoke(p1: P1, p2: P2): R
}      

invoke约定调用示例:

class Lambda2 {
    
    fun test() {
      val function1=  object : Function1<Int, Boolean> {
            override fun invoke(p1: Int): Boolean {
                println(p1)
                return true
            }
        }
        //对象 function1。
        function1(1)
    }

}
注意:
//这句话会被默认映射为
function1.invoke(1)      

函数类型对象调用示例

//function1:(Int)->Boolean  函数对象调用
    
    fun test2(function1:(Int)->Boolean){
        function1(1)
    }      

Lambda,除非是内联的,都是被编译成实现了函数式接口(Functionl 等)的类,而这些接

口定义了具有对应数量参数的 invoke 方法,如上 function1:(Int)->Boolean编译成字节码后会被系统使用function1函数进行替换。

使用invoke约定定义,可以将lambda函数体中抽取的方法的作用域尽可能的缩小,能够在不耦合外部逻辑的情况下实现代码解耦。

例子:

package dsl

data class Issue(val id:Int, val name:String)

class ImportantIssuesPredicate(val name:String):(Issue)->Boolean{
    override fun invoke(p1: Issue): Boolean {
       return p1.name.equals(name) && p1.isImport()
    }

    fun Issue.isImport(): Boolean {
       return name.equals(name) && id==1000
    }

}

fun main() {
    val iss1=Issue(1000,"test")
    val iss2=Issue(1001,"import")
    val importantIssuesPredicate=ImportantIssuesPredicate("test")

    for( issu in listOf<Issue>(iss1,iss2).filter(importantIssuesPredicate)){
        println(issu.name)
    }

}      

如上将 (Issue)->Boolean 最为基类。并复写invoke方法,并且定义了Issue.import( )扩展函数,将对比的代码抽取到mportantIssuesPredicate 函数类型对象中。如果不使用nvoke 约定,就需要将判断代码嵌套到for函数中。

DSL中的"invoke" 约定:在Gradle中的声明依赖

下面是开发中常见的依赖配置:

//扁平调用结构,只有一个依赖时可用
dependencies.compileOnly()
//嵌套代码块结构,有多个依赖时可用
dependencies {
    chinaImplementation "com.reworld.android:unity-sdk:1.0.0" 
    }      

如上: dependencies 对象是 DependencyHandler类型。

package dsl

class DependencyHandler {
    
    fun compile(dependency:String){
        println(dependency)
    }
    // DependencyHandler作为invoke函数入参的接收者类型。函数体中作为隐式接收者存在。
    operator fun invoke(body:DependencyHandler.()->Unit){
        body()
    }
}

fun main() {
    val dependencyHandler=DependencyHandler()
    dependencyHandler.compile("123")
    dependencyHandler{
        compile("123")
        compile("123")
        compile("123")
    }
}      

看到main()函数中写法和build.gradle 中脚本写法一致。通过这样设计的好处,有多个依赖就可以使用嵌套结构,一个依赖可以使用扁平调用结构

实践中Dsl用法

  1. 中缀调用链接起来:测试框架中的“should”

infix fun T.should(matcher:Matcher) =matcher.test(this)

infix中缀标示,可以不用写. 进行调用。

​​kotlintest​​

  1. 在基本类型上定义扩展:处理日期

val yesterday =1.days.ago

val tomorrow =1.days.fromNow

​​kxDate​​

  1. 为sql设计的内部Dsl

​​Exposed框架​​

  1. ​​Anko​​ :动态创建Android Ui

总结:

内部Dsl 是一种Api设计模式,借助多个方法调用组成的结构,可以使用这种模式来构建更表意的Api,带接收者lambda采用嵌套结构重新定义函数体中的方法如何解析。成员扩展函数依然收到容器的限制。与普通扩展函数具有不同的使用的场景。用作参数的带接收者的lambda,其类型是扩展函数类型,并且这个调用函数在调用lambda时会为它提供一个接收者实例。