本期文章來自獵蘿蔔內部技術分享

DSL(Domain Specified Language)領域專用語言 常見的DSL
  • 正則表達式

通過一些規定好的符號和組合規則,通過正則表達式引擎來實現字符串的匹配

  • HTML&CSS

雖然寫的是類似XML 或者 .{} 一樣的字符規則,但是最終都會被瀏覽器內核轉變成Dom樹,從而渲染到Webview上

  • SQL

雖然是一些諸如 create select insert 這種單詞後面跟上參數,這樣的語句實現了對數據庫的增刪改查一系列程序工作

DSL分類

  • 內部 DSL(從一種宿主語言構建而來)
  • 外部 DSL(從零開始構建的語言,需要實現語法分析器等)
通過Kotlin構建類型安全的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{ …… }}

(headbodyHTML的成員函數。)

現在,像往常一樣,this可以省略掉了,我們得到的東西看起來已經非常像一個構建器了:

html{ head{ …… } body{ …… }}

它創建了一個HTML的新實例,然後通過調用作爲參數傳入的函數來初始化它 (在我們的示例中,歸結爲在HTML實例上調用headbody),然後返回此實例。 這正是構建器所應做的。

HTML類中的headbody函數的定義與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無論是否被標記,都可以訪問接收器
完整代碼 packagecom.example.htmlinterface Element { funrender(builder: StringBuilder, indent: String)} classTextElement( valtext: String) : Element { override funrender(builder: StringBuilder, indent: String){builder.append( "$indent$textn") }}@DslMarkerannotation classHtmlTagMarker@HtmlTagMarkerabstract classTag( valname: String) : Element { valchildren= arrayListOf <Element>() valattributes= hashMapOf <String, String>() protected fun<T : Element>initTag(tag: T, init: T.()-> Unit): T {tag.init() children.add(tag) returntag } override funrender(builder: StringBuilder, indent: String){builder.append( "$indent<$name${renderAttributes()}>n") for(c inchildren) { c.render(builder, indent + " ") } builder.append( "$indent</$name>n") } private funrenderAttributes(): String { valbuilder= StringBuilder() for((attr, value) inattributes) { builder.append( " $attr="$value"") } returnbuilder.toString() } override funtoString(): String { valbuilder= StringBuilder() render(builder, "") returnbuilder.toString() }}abstract classTagWithText(name: String) : Tag(name) { operator funString.unaryPlus(){children.add(TextElement(this)) }} classHTML: TagWithText( "html") { funhead(init: Head.()-> Unit) = initTag(Head(), init) funbody(init: Body.()-> Unit) = initTag(Body(), init)} classHead: TagWithText( "head") { funtitle(init: Title.()-> Unit) = initTag(Title(), init)} classTitle: TagWithText( "title")abstract classBodyTag(name: String) : TagWithText(name) { funb(init: B.()-> Unit) = initTag(B(), init) funp(init: P.()-> Unit) = initTag(P(), init) funh1(init: H1.()-> Unit) = initTag(H1(), init) funa(href: String, init: A.()-> Unit) { vala= initTag(A(), init) a.href = href }} classBody: BodyTag( "body") classB: BodyTag( "b") classP: BodyTag( "p") classH1: BodyTag( "h1") classA: BodyTag( "a") { varhref: String get() = attributes[ "href"]!! set(value) { attributes[ "href"] = value }} funhtml(init: HTML.()-> Unit): HTML { valhtml= HTML() html.init() returnhtml} 應用
  • kotlin官方html構造器:kotlinx.html
  • kotlin的javaFX框架:TornadoFX
  • 安卓佈局框架:anko
  • kotlin服務端框架:ktor
參考文檔

類型安全的構建器

Scope control for implicit receivers

Kotlin之美——DSL篇

相關文章