關於Kotlin語法的異議

狀況

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

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

若函數的惟一或最後一個參數是函數類型,能夠不須要用括號圍住這個參數,這樣就能隨手寫出這樣漂亮的DSL:函數

// transaction(...)接受一個類型爲函數的參數
// Auto handle a transaction
transaction {
  saveData()
}

// use(...)接受一個類型爲函數的參數
// Auto close the resource
inputStream.use {
  consume(it)
}

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

// Java
String externalName;
{
  String internalName = getInternalName();
  externalName = convert(internalName);
}
// Scala
val externalName = {
  val internalName = getInternalName()
  convert(internalName)
}
// Kotlin
val externalName = run {
  val internalName = getInternalName()
  convert(internalName)
}

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

相似run的函數有好幾個,咱們來瞧一瞧。對象

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

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

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

把T做爲this,執行block,返回block的結果R

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

把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 }it

把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參數。

// 使用this
user.apply {
  name = "Mr Wang"
  password = "&%&&**("
}

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

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

// 不會執行
{
  doSomething()
}

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

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

相關文章
相關標籤/搜索