Kotlin 系列:
Kotlin DSL 把 Kotlin 的語(yǔ)法糖演繹得淋漓盡致赏胚,這些語(yǔ)法糖可謂好吃、好看又好玩疫向,但是舌涨,僅癡迷于語(yǔ)法糖只會(huì)對(duì)語(yǔ)言的理解游離于表面,了解其實(shí)現(xiàn)原理博其,是我們閱讀優(yōu)秀源碼息尺、設(shè)計(jì)整潔代碼和理解編程語(yǔ)言的必經(jīng)之路个绍,本文我們通過 DSL 來感受 Kotlin 之美钉迷。
理解 DSL
DSL(domain specific language)荒椭,即領(lǐng)域?qū)S谜Z(yǔ)言:專門解決某一特定問題的計(jì)算機(jī)語(yǔ)言,比如大家耳熟能詳?shù)?SQL 和正則表達(dá)式。
通用編程語(yǔ)言 vs DSL
通用編程語(yǔ)言(如 Java、Kotlin、Android等)淮悼,往往提供了全面的庫(kù)來幫助開發(fā)者開發(fā)完整的應(yīng)用程序,而 DSL 只專注于某個(gè)領(lǐng)域翘鸭,比如 SQL 僅支持?jǐn)?shù)據(jù)庫(kù)的相關(guān)處理,而正則表達(dá)式只用來檢索和替換文本浦妄,我們無(wú)法用 SQL 或者正則表達(dá)式來開發(fā)一個(gè)完整的應(yīng)用尼摹。
API vs DSL
無(wú)論是通用編程語(yǔ)言,還是領(lǐng)域?qū)S谜Z(yǔ)言剂娄,最終都是要通過 API 的形式向開發(fā)者呈現(xiàn)蠢涝。良好的、優(yōu)雅的阅懦、整潔的和二、一致的 API 風(fēng)格是每個(gè)優(yōu)秀開發(fā)者的追求,而 DSL 往往具備獨(dú)特的代碼結(jié)構(gòu)和一致的代碼風(fēng)格耳胎,從 SQL 和正則表達(dá)式的語(yǔ)法風(fēng)格便可感受一二惯吕。
下文我們也將提到,Kotlin 構(gòu)建的 DSL怕午,代碼風(fēng)格更具表現(xiàn)力和想象力废登,也更加優(yōu)雅。
內(nèi)部 DSL
但是郁惜,如果為解決某一特定領(lǐng)域問題就創(chuàng)建一套獨(dú)立的語(yǔ)言堡距,開發(fā)成本和學(xué)習(xí)成本都很高,因此便有了內(nèi)部 DSL 的概念。所謂內(nèi)部 DSL羽戒,便是使用通用編程語(yǔ)言來構(gòu)建 DSL缤沦。比如,本文提到的 Kotlin DSL易稠,我們?yōu)?Kotlin DSL 做一個(gè)簡(jiǎn)單的定義:
“使用 Kotlin 語(yǔ)言開發(fā)的缸废,解決特定領(lǐng)域問題,具備獨(dú)特代碼結(jié)構(gòu)的 API 驶社∑罅浚”
下面,我們就來領(lǐng)略下千變?nèi)f化的 Kotlin DSL 衬吆。
有趣的 Kotlin DSL
如果說 Kotlin 是一位魔術(shù)師梁钾,那么 DSL 便是其賴以成名,令人嘖嘖稱贊的魔術(shù)作品逊抡,我們先來看下 Kotlin 在各個(gè)特定領(lǐng)域的有趣實(shí)現(xiàn)姆泻。
- 日期
val yesterday = 1.days.ago // 也可以這樣寫: val yesterday = 1 days ago
val twoMonthsLater = 2 months fromNow
以上日期處理的代碼,真正做到見名知意冒嫡,深諳代碼整潔之道拇勃,更多細(xì)節(jié)可參考此庫(kù):kxdate 。
如果不考慮規(guī)范孝凌,基于該庫(kù)的設(shè)計(jì)思路方咆,我們甚至可以設(shè)計(jì)出如下的 api:
val yesterday = 1 天 前
val twoMonthsLater = 2 月 后
這個(gè)日期處理領(lǐng)域的 DSL 體現(xiàn)出來的代碼結(jié)構(gòu)是鏈?zhǔn)降模⑶医朴谖覀內(nèi)粘J褂玫挠⒄Z(yǔ)
- 單元測(cè)試
val str = "kotlin"
str should startWith("kot")
str.length shouldBe 6
與上述日期庫(kù)的 api 風(fēng)格類似蟀架,該單元測(cè)試的代碼也是賞心悅目瓣赂,更多細(xì)節(jié)可參考此庫(kù):kotlintest 。
基于該庫(kù)的設(shè)計(jì)思路片拍,我們甚至可以實(shí)現(xiàn)如下的代碼風(fēng)格煌集,如同寫英語(yǔ)句子一般簡(jiǎn)潔:
"kotlin" should start with "kot"
"kotlin" should have substring "otl"
這個(gè) DSL 的代碼結(jié)構(gòu)近似于我們?nèi)粘J褂玫挠⒄Z(yǔ)。
- HTML 構(gòu)建器
fun createTable() =
table{
tr{
td{
}
}
}
>>> println(createTable())
<table><tr><td></td></tr></table>
這個(gè) DSL 的代碼結(jié)構(gòu)使用了 lambda 嵌套捌省,并且語(yǔ)義清晰苫纤,一目了然。更多詳情參考此庫(kù):kotlinx.html纲缓。
- SQL
(Users innerJoin Cities).slice(Users.name, Cities.name).
select {(Users.id.eq("andrey") or Users.name.eq("Sergey")) and
Users.id.eq("sergey") and Users.cityId.eq(Cities.id)}.forEach {
println("${it[Users.name]} lives in ${it[Cities.name]}")
}
這類 SQL api 的風(fēng)格卷拘,如果有用過 ORM 的框架,如 ActiveAndroid 或者 Realm 就不會(huì)陌生祝高。以上代碼來自于此庫(kù):Exposed 栗弟。
- Android 布局
Anko Layouts 是一套幫助我們更簡(jiǎn)潔的開發(fā)和復(fù)用 Android 布局的 DSL ,它的代碼風(fēng)格如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!") }
}
}
}
}
相比于笨重的 XML 布局方式工闺,Anko DSL 顯然是更先進(jìn)和更高效的解決方案乍赫。
- Gradle 構(gòu)建
Gradle 的構(gòu)建腳本是 groovy颓屑,對(duì) Android 程序員有一定的學(xué)習(xí)成本,目前耿焊,Gradle 官方也提供了基于 Kotlin 的構(gòu)建腳本:Gradle Kotlin DSL , 并提供了類 groovy 的代碼風(fēng)格:
dependencies {
compile("com.android.support:appcompat-v7:27.0.1")
compile("com.android.support.constraint:constraint-layout:1.0.2")
}
完整代碼請(qǐng)參考:build.gradle.kts。
綜上遍搞,Kotlin DSL 所體現(xiàn)的代碼結(jié)構(gòu)有如下特點(diǎn):鏈?zhǔn)秸{(diào)用罗侯,大括號(hào)嵌套,并且可以近似于英語(yǔ)句子溪猿。
實(shí)現(xiàn)原理
看了那么多 Kotlin DSL 的風(fēng)格和使用場(chǎng)景钩杰,相較于刻板的、傳統(tǒng)的 Java 而言诊县,更加神奇和富有想象力讲弄。要理解 Kotlin DSL 這場(chǎng)魔術(shù)盛宴,就必須了解其背后用到的魔術(shù)道具——擴(kuò)展函數(shù)依痊、lambda避除、中綴調(diào)用和 invoke 約定。
擴(kuò)展函數(shù)(擴(kuò)展屬性)
對(duì)于同樣作為靜態(tài)語(yǔ)言的 Kotlin 來說胸嘁,擴(kuò)展函數(shù)(擴(kuò)展屬性)是讓他擁有類似于動(dòng)態(tài)語(yǔ)言能力的法寶瓶摆,即我們可以為任意對(duì)象動(dòng)態(tài)的增加函數(shù)或?qū)傩浴?/p>
比如,為 String 擴(kuò)展一個(gè)函數(shù): lastChar()
:
package strings
fun String.lastChar(): Char = this.get(this.length - 1)
調(diào)用擴(kuò)展函數(shù):
>>> println("Kotlin".lastChar())
n
與 JavaScript 這類動(dòng)態(tài)語(yǔ)言不一樣性宏,Kotlin 實(shí)現(xiàn)原理是: 提供靜態(tài)工具類群井,將接收對(duì)象(此例為 String )做為參數(shù)傳遞進(jìn)來,以下為該擴(kuò)展函數(shù)編譯成 Java 的代碼
/* Java */
char c = StringUtilKt.lastChar("Java");
回顧前文講到的日期的 DSL:
val yesterday = 1.days.ago
為配合擴(kuò)展函數(shù),我們先降低 api 的整潔程度毫胜,先實(shí)現(xiàn)一個(gè)擴(kuò)展函數(shù)的版本:
val yesterday = 1.days().ago()
1 為 Int 類型书斜,顯然 Int 并沒有 days()
函數(shù),因此days()
為擴(kuò)展函數(shù)酵使,偽代碼如下:
fun Int.days() = {//邏輯實(shí)現(xiàn)}
結(jié)合 Java8 的 Time api荐吉,此處將會(huì)涉及到兩個(gè)擴(kuò)展函數(shù),完整實(shí)現(xiàn)如下:
fun Int.days() = Period.ofDays(this)
fun Period.ago() = LocalDate.now() - this
若要實(shí)現(xiàn)最終的效果凝化,實(shí)際上就是將擴(kuò)展函數(shù)修改為擴(kuò)展屬性的方式即可(擴(kuò)展屬性需提供getter或setter稍坯,本質(zhì)上等同于擴(kuò)展函數(shù)):
val Int.days:Period
get() = Period.ofDays(this)
val Period.ago:LocalDate
get() = LocalDate.now() - this
代碼雖少,卻天馬行空搓劫,妙趣橫生瞧哟。
lambda
lambda 為 Java8 提供的新特性,于2014年3月18日發(fā)布枪向。在2018年的今天我們依然無(wú)法使用或者要花很大的代價(jià)才能在 Android 編程中使用勤揩,而 Kotlin 則幫助我們解決了這一瓶頸,這也是我們擁抱 Kotlin 的原因之一秘蛔。
lambda 是構(gòu)建整潔代碼的一大利器陨亡。
1. lambda 表達(dá)式
下圖是 lambda 表達(dá)式傍衡,他總是用一對(duì)大括號(hào)包裝起來,可以作為值傳遞給下節(jié)要提到的高階函數(shù)负蠕。
2. 高階函數(shù)
關(guān)于高階函數(shù)的定義蛙埂,參考《Kotlin 實(shí)戰(zhàn)》:
高階函數(shù)就是以另一個(gè)函數(shù)作為參數(shù)或返回值的函數(shù)
如果用 lamba 來作為高價(jià)函數(shù)的參數(shù)(此時(shí)為形參),就必須先了解如何聲明一個(gè)函數(shù)的形參類型遮糖,如下:
相對(duì)于上一小節(jié)绣的,我們應(yīng)該弄清楚 lambda 作為實(shí)參和形參時(shí)的表現(xiàn)形式:
// printSum 為高階函數(shù),定義了 lambda 形參
fun printSum(sum:(Int,Int)->Int){
val result = sum(1, 2)
println(result)
}
// 以下 lambda 為實(shí)參欲账,傳遞給高階函數(shù) printSum
val sum = {x:Int,y:Int->x+y}
printSum(sum)
有了高階函數(shù)屡江,我們可以很輕易地做到一個(gè) lambda 嵌套另一個(gè) lambda 的代碼結(jié)構(gòu)。
3. 大括號(hào)放在最后
Kotlin 的 lambda 有個(gè)規(guī)約:如果 lambda 表達(dá)式是函數(shù)的最后一個(gè)實(shí)參赛不,則可以放在括號(hào)外面惩嘉,并且可以省略括號(hào),如:
person.maxBy({ p:Person -> p.age })
// 可以寫成
person.maxBy(){
p:Person -> p.age
}
// 更簡(jiǎn)潔的風(fēng)格:
person.maxBy{
p:Person -> p.age
}
這個(gè)規(guī)約是 Kotlin DSL 實(shí)現(xiàn)嵌套結(jié)構(gòu)的本質(zhì)原因踢故,比如上文提到的 anko Layout:
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!") }
}
}
這里 verticalLayout 中 嵌套了 button文黎,想必該庫(kù)定義了如下函數(shù):
fun verticalLayout( ()->Unit ){
}
fun button( text:String,()->Unit ){
}
verticalLayout 和 button 均是高階函數(shù),結(jié)合大括號(hào)放在最后的規(guī)約畴椰,就形成了 lambda 嵌套的語(yǔ)法結(jié)構(gòu)臊诊。
4. 帶接收者的 lambda
lambda 作為形參函數(shù)聲明時(shí),可以攜帶接收者斜脂,如下圖:
帶接收者的 lambda 豐富了函數(shù)聲明的信息抓艳,當(dāng)傳遞該 lambda值時(shí),將攜帶該接收者帚戳,比如:
// 聲明接收者
fun kotlinDSL(block:StringBuilder.()->Unit){
block(StringBuilder("Kotlin"))
}
// 調(diào)用高階函數(shù)
kotlinDSL {
// 這個(gè) lambda 的接收者類型為StringBuilder
append(" DSL")
println(this)
}
>>> 輸出 Kotlin DSL
總而言之玷或,lambda 在 Kotlin 和 Kotlin DSL 中扮演著很重要的角色,是實(shí)現(xiàn)整潔代碼的必備語(yǔ)法糖片任。
中綴調(diào)用
Kotlin 中有種特殊的函數(shù)可以使用中綴調(diào)用偏友,代碼風(fēng)格如下:
"key" to "value"
// 等價(jià)于
"key.to("value")
而 to() 的實(shí)現(xiàn)源碼如下:
infix fun Any.to(that:Any) = Pair(this,that)
這段源碼理解起來不難,infix 修飾符代表該函數(shù)支持中綴調(diào)用对供,然后為任意對(duì)象提供擴(kuò)展函數(shù) to位他,接受任意對(duì)象作為參數(shù),最終返回鍵值對(duì)产场。
回顧下我們上文提到的不太規(guī)范的中文 api:
val yesteraty = 1 天 前
使用擴(kuò)展函數(shù)和中綴調(diào)用便可實(shí)現(xiàn):
object 前
infix fun Int.天(ago:前) = LocalDate.now() - Period.ofDays(this)
再比如上文提到的:
"kotlin" should start with "kot"
// 等價(jià)于
"kotlin".should(start).with("kot")
使用兩個(gè)中綴調(diào)用便可實(shí)現(xiàn)鹅髓,以下是偽代碼:
object start
infix fun String.should(start:start):String = ""
infix fun String.with(str:String):String = ""
所以,中綴調(diào)用是實(shí)現(xiàn)類似英語(yǔ)句子結(jié)構(gòu) DSL 的核心京景。
invoke 約定
Kotlin 提供了 invoke 約定窿冯,可以讓對(duì)象向函數(shù)一樣直接調(diào)用,比如:
class Person(val name:String){
operator fun invoke(){
println("my name is $name")
}
}
>>>val person = Person("geniusmart")
>>> person()
my name is geniusmart
回顧上文提到的 Gradle Kotlin DSL:
dependencies {
compile("com.android.support:appcompat-v7:27.0.1")
compile("com.android.support.constraint:constraint-layout:1.0.2")
}
// 等價(jià)于:
dependencies.compile("com.android.support:appcompat-v7:27.0.1")
dependencies.compile("com.android.support.constraint:constraint-layout:1.0.2")
這里确徙,dependencies 是一個(gè)實(shí)例醒串,既可以調(diào)用成員函數(shù) compile执桌,同時(shí)也可以直接傳遞 lambda 參數(shù),后者便是采用了 invoke 約定芜赌,實(shí)現(xiàn)原理簡(jiǎn)化如下:
class Dependencies{
fun compile(coordinate:String){
println("add $coordinate")
}
operator fun invoke(block:Dependencies.()->Unit){
block()
}
}
>>>val dependencies = Dependencies()
>>>// 以兩種方式分別調(diào)用 compile()
invoke 約定讓對(duì)象調(diào)用函數(shù)的語(yǔ)法結(jié)構(gòu)更加簡(jiǎn)潔仰挣。
總結(jié)
細(xì)細(xì)品味 Kotlin,你會(huì)發(fā)現(xiàn)她將代碼整潔之道(Clean Code)和高效 Java 編程(Effective Java)中的部分精華融入到的語(yǔ)法和默認(rèn)的規(guī)約中缠沈,因此她可以讓開發(fā)者無(wú)形中寫出整潔和高效的代碼椎木。
而更進(jìn)一步, Kotlin DSL 則是對(duì) Kotlin 所有語(yǔ)法糖的一個(gè)大融合博烂,她的代碼結(jié)構(gòu)通常是鏈?zhǔn)秸{(diào)用、lambda 嵌套漱竖,并且接近于日常使用的英語(yǔ)句子禽篱,我們可以愉悅的使用 DSL 風(fēng)格的 API,同時(shí)馍惹,也可以以此為思路躺率,為社區(qū)貢獻(xiàn)各種 Kotlin DSL。
Kotlin DSL 體現(xiàn)了代碼的整潔之道万矾,體現(xiàn)了天馬行空的想象力悼吱,在 DSL 的點(diǎn)綴下,Kotlin 顯示出整潔的美良狈,自由的美后添。
Kotlin 有趣的外表之下,是一個(gè)更有趣的靈魂薪丁。
參考資料
- 《Kotlin 實(shí)戰(zhàn)》