关于Kotlin语法的异议

sorra 发表于 09/01 20:22 修改于 09/02 08:33 阅读数71

情况

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

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开发组的一种取舍:

// 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家族不一致,而且有点繁琐。

类似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参数。

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

// 使用it必须显式声明
user.apply { it ->
  it.name = ""
  it.password = "&%&&**("
}

另一个问题,从Java转过来的应用开发者可能会习惯性地写一个代码块,忘了这实际上是一个Lambda,是不会执行的。

// 不会执行
{
  doSomething()
}

// 会执行
{
  doSomething()
}()

原则上应禁止定义未被使用的Lambda,以免误写出永远不会被执行的代码。