標籤:

關於Kotlin語法的異議

情況

Lambda的表示法是{ ... } ,例如:

val func = { println()}val func = { x -> println(x)}

若函數的唯一或最後一個參數是函數類型,可以不需要用括弧圍住這個參數,這樣就能隨手寫出這樣漂亮的DSL:

// transaction(...)接受一個類型為函數的參數// Auto handle a transactiontransaction { saveData()}// use(...)接受一個類型為函數的參數// Auto close the resourceinputStream.use { consume(it)}

但是就不能像C/Java/Scala一樣用花括弧把代碼組織成代碼塊了,而是必須調用run函數,這是Kotlin開發組的一種取捨:

// JavaString externalName;{ String internalName = getInternalName(); externalName = convert(internalName);}// Scalaval externalName = { val internalName = getInternalName() convert(internalName)}// Kotlinval externalName = run { val internalName = getInternalName() convert(internalName)}

run函數會被編譯器內聯,沒有函數調用的額外開銷,只是語法上與C家族不一致,而且有點繁瑣。

類似run的函數有好幾個,我們來瞧一瞧。

run(block: () -> R): R = block()

執行block,返回block的結果R,相當於Scala的普通代碼塊

T.run(block: T.() -> R): R = block()

把T作為this,執行block,返回block的結果R

T.let(block: (T) -> R): R = block(this)

把T作為block的入參(it),執行block,返回block的結果R

T.apply(block: T.() -> Unit): T { block(); return this }

把T作為this,執行block,返回T本身

T.also(block: (T) -> Unit): T { block(this); return this }

把T作為block的入參(it),執行block,返回block的結果R

with(receiver: T, block: T.() -> R): R = receiver.block()

把第一個參數作為this傳給block。能表達這種代碼:with(entry) { consumeKeyValue(key, value) }

當我們想隨手轉換一個對象,可以這麼寫:

val userDTO = user.run { UserDTO(name, password)}

或這麼寫

val userDTO = user.let { UserDTO(it.name, it.password)}

當我們新建了一個對象,想隨手給它完成初始化,可以這麼寫:

val user = User().apply { name = "Mr Wang" password = "&%&&**("}

或這麼寫

val user = User().let { it.name = "Mr Wang" it.password = "&%&&**("}

你可能覺得這只是微小的語法糖,那麼看這個nullable的例子:

// 繁瑣寫法val userDTO = if (user != null) { UserDTO(user.name, user.password) } else { null }// 簡潔寫法val userDTO = user?.run { UserDTO(name, password)}

是不是高下立見?

批評

為了支持多種寫法,佔用了很多名字。像run這種在JDK中常見的名字也被用了,雖然有靜態檢查,但同一個名字被安上不同的語義仍然是一種心智負擔。況且run這個名字完全沒有表達「轉換」的意思嘛!

使用函數之前要先想好接下來的寫法適合哪個名字的函數,也是一種心智負擔。

為了使語義更明確,我建議做如下改進:

減少內置函數名:去掉run和also,只保留let,apply和with。採用如下幾種函數簽名:

let(block: () -> R)

T.let(block: T.() -> R) T.let(block: (T) -> R): R

T.apply(block: T.() -> Unit): T T.apply(block: (T) -> Unit): T

with(receiver: T, block: T.() -> R): R

如上,with沒有變,run被併入let,also被併入apply。

let表示定義新的變數,apply表示對現有變數做一些處理,with表示以現有變數作為scope來做一些事(包括返回新的變數)。語義很清楚。

let總是返回結果R,apply總是返回原本的T。let/apply若接受無參函數,就把T作為this,若接受唯一參數為T的函數,就把T作為參數傳入,不允許隱式的it參數。

// 使用thisuser.apply { name = "Mr Wang" password = "&%&&**("}// 使用it必須顯式聲明user.apply { it -> it.name = "" it.password = "&%&&**("}

另一個問題,從Java轉過來的應用開發者可能會習慣性地寫一個代碼塊,忘了這實際上是一個Lambda,是不會執行的。

// 不會執行{ doSomething()}// 會執行{ doSomething()}()

原則上應禁止定義未被使用的Lambda,以免誤寫出永遠不會被執行的代碼。


推薦閱讀:

Android 坑檔案:背黑鍋的 Fragment
如何評價開中文C# issue的人到kotliner.cn論壇提中文版Kotlin?
Android 開發者自述:為什麼我要改用 Kotlin?
Kotlin 互動式命令行工具:kotlinc
從零開始自製遊戲引擎 IV

TAG:Kotlin |