天天看點

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時會為它提供一個接收者執行個體。