用Kotlin構建神奇的DSL
DSL(Domain Specified Language)領域專用語言 常見的DSL本期文章來自獵蘿蔔內部技術分享
- 正則表達式
通過一些規定好的符號和組合規則,通過正則表達式引擎來實現字符串的匹配
- HTML&CSS
雖然寫的是類似XML 或者 .{} 一樣的字符規則,但是最終都會被瀏覽器內核轉變成Dom樹,從而渲染到Webview上
- SQL
雖然是一些諸如 create select insert 這種單詞後面跟上參數,這樣的語句實現了對數據庫的增刪改查一系列程序工作
DSL分類
- 內部 DSL(從一種宿主語言構建而來)
- 外部 DSL(從零開始構建的語言,需要實現語法分析器等)
例子:
html { head { title { +"XML encoding with Kotlin" } } body { h1 { +"XML encoding with Kotlin" } p { +"this format can be used as an alternative markup to XML" } // 一個具有屬性和文本內容的元素 a(href = "http://kotlinlang.org") { +"Kotlin" } // 混合的內容 p { +"This is some" b { +"mixed" } +"text. For more see the" a(href = "http://kotlinlang.org") { +"Kotlin" } +"project" } p { +"some text" } // 以下代碼生成的內容 p { for (arg in args) +arg } } }
這是在kotlin中完全合法的一段代碼,並且可以正確運行出結果,得到的結果如下圖
<html><head><title>XML encoding with Kotlin </title></head><body><h1>XML encoding with Kotlin </h1><p>this format can be used as an alternative markup to XML </p><ahref="http://kotlinlang.org">Kotlin </a><p>This is some <b>mixed </b>text. For more see the <ahref="http://kotlinlang.org">Kotlin </a>project </p><p>some text </p><p></p></body></html>
這就是我們自定義DSL構造器得出的結果。
首先我們回顧一些kotlin技術:
lambda與高階函數
Kotlin 的 lambda 有個規約:如果 lambda 表達式是函數的最後一個實參,則可以放在括號外面,並且可以省略括號,這個規約是 Kotlin DSL 實現嵌套結構的本質原因。
傳遞lambda表達式作爲參數:`fun html(init: HTML.() -> Unit): HTML,這個方法接收一個有receiver的lambda表達式,因爲這樣在block的內部就可以直接訪問receiver的公共成員了,這一點也很重要。
擴展函數(擴展屬性)
對於同樣作爲靜態語言的 Kotlin 來說,擴展函數(擴展屬性)是讓他擁有類似於動態語言能力的法寶,即我們可以爲任意對象動態的增加函數或屬性。
比如,爲 LocalDate 擴展一個函數:toDate():
funLocalDate.toDate(): Date = Date.from(this.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())
與 Java 這類動態語言不一樣,Kotlin 實現原理是: 提供靜態工具類,將接收對象(此例爲 String )做爲參數傳遞進來,以下爲該擴展函數編譯成 Java 的代碼
@NotNullpublicstaticfinalDate toDate(@NotNull LocalDate $receiver){ Intrinsics.checkParameterIsNotNull($receiver, "$receiver"); Date var10000 = Date.from($receiver.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Intrinsics.checkExpressionValueIsNotNull(var10000, "Date.from(this.atStartOf…emDefault()).toInstant())"); returnvar10000; } //java callDate date = toDate(LocalDate.now()); //kotlin callval date = LocalDate.now().toDate()
在Kotlin語言中,類不再是語言的最小單位。我們既可以單獨聲明一個全局函數,也可以聲明全局變量。因此,你可以認爲toLong是一個函數整體,這個函數的接收者可以是任意對象。
而對於Java,Java語言中所有的行爲都必須在類體中完成。具體到某個函數或某一個變量始終屬於某一個類實例。換而言之,其Receiver是固定的,也就沒有了所謂Receiver的概念。
一元運算符 operator funBigDecimal.unaryPlus()= this.plus(java.math.BigDecimal.TEN)println(+BigDecimal( "100")) //110開始分析 實現原理
首先看
html{ head{} body{}}
這個代碼塊的實質是一個函數調用
funhtml(init: HTML.()-> Unit): HTML {valhtml= HTML() html.init() returnhtml}
這個函數接受一個名爲init的參數,該參數本身就是一個函數。 該函數的類型是HTML.() -> Unit,它是一個帶接收者的函數類型。 這意味着我們需要向函數傳遞一個 HTML 類型的實例(接收者), 並且我們可以在函數內部調用該實例的成員。 該接收者可以通過this關鍵字訪問:
html{ this.head { …… } this. body{ …… }}
(head和body是HTML的成員函數。)
現在,像往常一樣,this可以省略掉了,我們得到的東西看起來已經非常像一個構建器了:
html{ head{ …… } body{ …… }}
它創建了一個HTML的新實例,然後通過調用作爲參數傳入的函數來初始化它 (在我們的示例中,歸結爲在HTML實例上調用head和body),然後返回此實例。 這正是構建器所應做的。
HTML類中的head和body函數的定義與html類似。 唯一的區別是,它們將構建的實例添加到包含HTML實例的children集合中:
funhead(init: Head.()-> Unit) : Head {valhead= Head() head.init() children.add(head) returnhead} funbody(init: Body.()-> Unit) : Body {valbody= Body() body.init() children.add(body) returnbody}
實際上這兩個函數做同樣的事情,所以我們可以有一個泛型版本,initTag:
protectedfun<T : Element>initTag(tag: T, init: T.()-> Unit): T {tag.init() children.add(tag) returntag}
所以,現在我們的函數很簡單:
funhead(init: Head.()-> Unit) = initTag(Head(), init)funbody(init: Body.()-> Unit) = initTag(Body(), init)
並且我們可以使用它們來構建<head>和<body>標籤。
這裏要討論的另一件事是如何向標籤體中添加文本。在上例中我們這樣寫到:
html { head { title {+ "XML encoding with Kotlin"} } // ……}
所以基本上,我們只是把一個字符串放進一個標籤體內部,但在它前面有一個小的+, 所以它是一個函數調用,調用一個前綴unaryPlus()操作。 該操作實際上是由一個擴展函數unaryPlus()定義的,該函數是TagWithText抽象類(Title的父類)的成員:
operator funString.unaryPlus(){children.add(TextElement(this))}
所以,在這裏前綴+所做的事情是把一個字符串包裝到一個TextElement實例中,並將其添加到children集合中, 以使其成爲標籤樹的一個適當的部分。
作用域控制
由於內部的作用域默認可以獲得外部的隱式接收器
html{ head{ head{ //無意義的head} } }
我們可以使用@DslMarker來註釋一個註解
@Target(ANNOTATION_CLASS)@Retention(BINARY)@MustBeDocumented@SinceKotlin( "1.1") publicannotation classDslMarker@DslMarkerannotation classHtmlTagMarker
註釋類HtmlTagMarker被稱爲一個DSL標記,它被註解@DslMarker註釋。
一般規則:
- 如果隱式接收器用@HtmlTagMarker相應的DSL標記註釋標記,則它可以屬於 DSL
- 同一DSL的兩個隱式接收器在同一範圍內不可訪問
- 就近原則
- 其他可用的接收器可以照常解析,但如果得到的解析調用綁定到這樣的接收器,則編譯錯誤
標記規則:隱式接收器被視爲被@HtmlTagMarker註釋,需要滿足下面的條件:
- 它的類型是被標記,或
- 它的類型分類器被標記
- 或其任何超類/超接口
補充說明
- this@label無論是否被標記,都可以訪問接收器
- kotlin官方html構造器:kotlinx.html
- kotlin的javaFX框架:TornadoFX
- 安卓佈局框架:anko
- kotlin服務端框架:ktor
類型安全的構建器
Scope control for implicit receivers
Kotlin之美——DSL篇