Kotlin Scoping 函数

Wafer Li ... 2017-04-22 21:02 Kotlin
  • Kotlin
大约 5 分钟

在 Kotlin 的 Standard.kt (opens new window) 中提供了一些特殊的高阶函数;

它们被称作 Scoping 函数,此类函数通过使用一个函数 block,将你需要对某对象进行的一系列操作限制在 lambda 作用域内;

这样,对于该对象操作的代码就不会泄露到外层作用域,使得代码更为干净整洁。

例如:

DbConnection.getConnection().let { connection ->
}
// connection is no longer visible here
1
2
3

可以看到,对于 connection 的操作就仅局限于 let 的 lambda 区域,而在 lambda 区域外是不可见的;

这就可以保证对 connection 的操作,不会影响到接下来的作用域。

# 1. let

# 1.1 定义

public inline fun <T, R> T.let(f: (T) -> R): R = f(this)
1

# 1.2 例子

val s = "hoge".let { it.toUpperCase() }
println(s) //=> HOGE
1
2

从定义之中我们可以看到,let 是所有类型都具有的扩展函数;

它的 lambda 的参数就是 let 的调用者。

# 1.3 主要用途

let 的主要用途在 Kotlin 的 Idioms (opens new window) 中有介绍;

主要就是用于在对象 nullable 的时候,对对象进行操作;

data?.let {
    ... // execute this block if not null
}
1
2
3

datanull 时,let 就不执行,而直接返回 null

否则就执行 let 的 lambda。

此时,它与 Java Optional 的以下三个函数的功能类似:

  • map
  • flatMap
  • ifPresent

可以看到,let 实际上就相当于集合中的 map,作用就是进行元素的变换功能;

注意,不能在 let 中调用 it 的修改方法;

否则,就会对原有对象进行改变。

# 2. with

# 2.1 定义

public inline fun <T, R> with(receiver: T, f: T.() -> R): R = receiver.f()
1

# 2.2 例子

val w = Window()
with(w) {
  setWidth(100)
  setHeight(200)
  setBackground(RED)
}
1
2
3
4
5
6

let 不同,with 并不是扩展函数

它的第一个参数,是任意类型的对象,如上面的 x

需要注意的是它的 lambda 部分,它的 lambda 要求接收者(调用者)必须是第一个参数的类型;

也就是说,我们可以在它的 lambda 中调用第一个参数的方法;

正如上面的例子,其中的几个 set 方法都隐含了调用者是 w

# 2.3 主要用途

由于指定了接收者类型,所以 with 函数主要用于对复杂对象的一系列配置操作。

如上面的设置 Window 的宽度和高度,以及背景颜色等。

可以看到,with 的调用 会改变传入的对象

实际上,这里也可以使用 let 函数进行这种操作;

不过由于 let 函数是将对象当做 参数 传入,所以如果要获得和 with 一样的效果,就必须在前面加 it

所以,let 并不适合这里所说的这个用途,利用 let 进行元素变换即可。

# 3. run

# 3.1 定义

public inline fun <T, R> T.run(f: T.() -> R): R = f()
1

# 3.2 例子

val s = "hoge".run { toUpperCase() }
println(s) //=> HOGE
1
2

可以看到,run 实际上就是 letwith 的结合;

可以让 with 不需要指定 receiver 参数就进行对象内部属性的配置;

同时,run 也是一个扩展函数,可以通过任何的类进行调用。

# 3.3 主要用途

作为 letwith 的合体方法,那么最主要的用途当然还是进行某个对象的配置。

需要注意的是,run 也会对对象进行改变。

# 4. apply

# 4.1 定义

public inline fun <T> T.apply(f: T.() -> Unit): T { f(); return this }
1

# 4.2 例子

val s = "hoge".apply { toUpperCase() }
println(s) //=> hoge
1
2

相比之前的结果,返回的依旧是小写字符;

这是由于 apply 返回的是 apply 的调用者的缘故。

# 4.3 主要用途

由于 apply 的返回类型为调用者自身,所以可以利用 apply 实现一个 流式 API 调用

实际上就是 with 最后返回 this 的简略版本。

# 5. also

这是 Kotlin 1.1 新增的 scoping 函数

# 5.1 定义

public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
1

# 5.2 使用例子

val s = "hoge".also { it.toUpperCase() }
println(s) //=> hoge
1
2

可以看到,其作用和 apply 一样;

但是和 apply 的区别在于,also 的函数参数并非指定接收者;

而是将调用者 T 当做其参数传入 lambda;

类似于 letapply 版本。

# 5.3 主要用途

那么这样做有什么好处呢?

首先,由于 没有指定接收者,所以 lambda 内外的 this 的含义没有改变:

// applyを使用
val button = Button(this).apply {
  text = "Click me"
  setOnClickListener {
    startActivity(Intent(this@MainActivity, NextActivity::class.java))
    // 単なる「this」ではNG   ^
  }
}

// alsoを使用
val button = Button(this).also { button ->
  button.text = "Click me"
  button.setOnClickListener {
    startActivity(Intent(this, NextActivity::class.java))
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

其次,可以通过赋予 lambda 参数名字,例如上面的 button ,增强可读性。

# 5.4 和 let 的区别

alsolet 都是通过将调用者作为 lambda 的参数传入函数的形式进行调用;

其区别就在于 also 最终返回值为其自身的调用者,即 this

let 的最终返回值由它的 lambda 的最后一个表达式的返回值决定。

类似于 applywith 的区别;

同理,也可以利用 let 来实现上面的 also 实现的功能:

val button = Button(this).let { button ->
  button.text = "Click me"
  button.setOnClickListener {
    startActivity(Intent(this, NextActivity::class.java))
  }
  button // letの場合はこれが必要になる
}
1
2
3
4
5
6
7

# 6. 总结

  1. let 用于进行元素变换操作,类似于 map
  2. with 用于对复杂对象的配置,需要提供具体的对象
  3. runwithlet 版本,配置对象属性,不需要提供具体对象
  4. applywith 的流式 API 版本
  5. alsoletapply 版本,用于对象配置,同时保留流式 API 和 当前 this 的含义

# 7. 参考资料

Kotlin スコープ関数 用途まとめ (opens new window)

Exploring the Kotlin standard library (opens new window)