結論(2021-07-06更新)
個人的なスタンス
個人的な意見として、原則 nullableのためにletを使うのみに留めたいところです。
そもそも、このような記事を書いて、それなりに閲覧されている程度には、スコープ関数は難しいからです。
JavaのAPIを使う場合、alsoやapplyが便利な場面があって、これは例外的に使ってもいいかもしれません。
が、スコープ関数を使わない素直なコードの選択肢を常に持っておくべきでしょう。
一応 スコープ関数の図解
--------以下、詳しく知りたい人向け--------
Kotlinの標準ライブラリには「スコープ関数」と呼ばれる4つの関数があります。
let、with、run、applyです。
それぞれ似ているので使い分けが難しいと思い、私なりの考えをまとめておきます。
(2017-01-26 追記) ver1.1で追加されるalsoについて解説を追加しました。
(2017-06-08 追記) 「スコープ関数」という名称の由来について追記しました。
let
定義
public inline fun <T, R> T.let(f: (T) -> R): R = f(this)
使用例
val s = "hoge".let { it.toUpperCase() }
println(s) //=> HOGE
定義からもわかりますがletは任意の型の拡張関数です。
上記の例ではStringインスタンス"hoge"をレシーバとしています。
letは引数として関数を取ります。
この関数はletのレシーバを受け取り、任意の型を返します。
主な用途
nullableな変数に対して使うことが多いと思います。
nullableと組み合わせて使うことによりletはJavaのOptionalにおける次の3つのメソッドの役割と似ています。
mapflatMapifPresent
例えばString?な変数fooの大文字表現を得たい場合にはこう記述できます。
val upperCase: String? = foo?.let { it.toUpperCase() }
fooがnullである場合には?.呼び出しによりletを実行せずnullを返します。
fooがnullでない場合にはletが実行されit.toUpperCase()によりfooの大文字表現が返されます。
蛇足ですが、上記のようなケースではfoo?.let(String::toUpperCase)と記述できます。
with
定義
public inline fun <T, R> with(receiver: T, f: T.() -> R): R = receiver.f()
使用例
val s = with("hoge") { this.toUpperCase() }
println(s) //=> HOGE
withはletとは異なり拡張関数ではありません。
第一引数に任意の型Tを取ります。
第二引数に関数を取りますが、Tをレシーバとするメソッドである必要があります。
上記の例における{ this.toUpperCase() }のthisは"hoge"を指します。
もちろんthisは省略可能なので{ toUpperCase() }と記述してもOKです。
主な用途
あるインスタンスに対する複数の操作をすっきり記述することが目的だと思われます。
例えばインスタンスを生成し、各種設定を行ってから実際に使用するという場面は多いです。
val frame: JFrame = with(JFrame("My App")) {
size = Dimension(600, 400)
defaultCloseOperation = JFrame.EXIT_ON_CLOSE
setVisible(true)
this
}
sizeプロパティやsetVisibleメソッドはJFrameのメンバです。
つまりこの文脈でレシーバはthisとなり、省略可能なのでレシーバを繰り返し記述する必要がなくなります。
この用途のためにletを使うことは適していません。
なぜならsizeやsetVisibleにアクセスする際にはレシーバとしてitが必須でありit.size、it.setVisible(true)のように記述する必要があるからです。
上記の例のようにwithの引数として渡したインスタンスをごにょごにょした後に、そのインスタンス自体を返す場合は後述のapplyの方が適しているかも知れません。
run
定義
public inline fun <T, R> T.run(f: T.() -> R): R = f()
使用例
val s = "hoge".run { toUpperCase() }
println(s) //=> HOGE
letとwithが合わさったような定義になっています。
runは任意の型Tの拡張関数で、そのTをレシーバとするメソッドのような関数を引数に取ります。
主な用途
用途もletとwithの合体版だと思います。
val frame: JFrame? = frameOrNull()
frame?.run {
size = Dimension(600, 400)
defaultCloseOperation = JFrame.EXIT_ON_CLOSE
setVisible(true)
}
apply
定義
public inline fun <T> T.apply(f: T.() -> Unit): T { f(); return this }
使用例
val s = "hoge".apply { toUpperCase() }
println(s) //=> hoge
今回は他の使用例と変わり、変数sは小文字のままです。
applyは引数として受け取った関数を実行して何か仕事をしますが、返す値はレシーバだからです。
主な用途
withの、レシーバを返して欲しいときに使う版と言えます。
withを使用したコードと見比べてください。
val frame: JFrame = JFrame("My App").apply {
size = Dimension(600, 400)
defaultCloseOperation = JFrame.EXIT_ON_CLOSE
setVisible(true)
}
withを使用したバージョンでは、引数の関数内の最後に戻り値としてthisと明記していました。
applyはレシーバを返すのでthisと明記する手間が省けます。
Android開発者にはおなじみだと思いますが、
Fragmentをnewして、argumentsをセットするパターンを実装する際に効果を発揮します。
class MyFragment: Fragment() {
companion object {
fun new(foo: Int, bar: Int): MyFragment {
val args = Bundle()
args.putInt("foo", foo)
args.putInt("bar", bar)
val myFragment = MyFragment()
myFragment.arguments = args
return myFragment
}
}
}
class MyFragment: Fragment() {
companion object {
fun new(foo: Int, bar: Int): MyFragment =
MyFragment().apply {
arguments = Bundle().apply {
putInt("foo", foo)
putInt("bar", bar)
}
}
}
}
実際のプロダクトでは
手元のKotlinソースコードが7000行ほどのAndroidアプリで、どの程度スコープ関数が使われているか調べてみました。
| 関数 | 使用回数 |
|---|---|
| let | 54 |
| with | 0 |
| run | 40 |
| apply | 37 |
withは1回も使ってませんが、それ以外は、けっこう使ってるんですね。
スコープ関数の便利さをお伝えできれば嬉しいです。
also
ver 1.1から標準ライブラリに加わる拡張関数です。
定義
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
使用例と主な用途
val s = "hoge".also { it.toUpperCase() }
println(s) //=> hoge
定義や上記の例からもわかるように、applyとほぼ同じです。異なる点は「引数が元のレシーバ("hoge")の拡張関数ではなく、元のレシーバを引数に取る関数である」というところです。その利点は2つあります。
まず、名前を付けられることです。引数の関数をラムダ式として記述した場合、元のレシーバをitとして参照できますが、何か意味のある名前を付けてやれば(例えばnameとかlabelとか)、コードの可読性が上がるでしょう。
そしてもう一つの利点は、ラムダ式の内と外でthisの意味が変わらないということです。
val button = Button(this).apply {
text = "Click me"
setOnClickListener {
startActivity(Intent(this@MainActivity, NextActivity::class.java))
// 単なる「this」ではNG ^
}
}
val button = Button(this).also { button ->
button.text = "Click me"
button.setOnClickListener {
startActivity(Intent(this, NextActivity::class.java))
}
}
同じく「元のレシーバを引数に取る関数」を引数に取るletとの違いは、letは戻り値を自分で決めるのに対し、alsoは元のレシーバが返されるという点です。
val button = Button(this).let { button ->
button.text = "Click me"
button.setOnClickListener {
startActivity(Intent(this, NextActivity::class.java))
}
button // letの場合はこれが必要になる
}
「スコープ関数」という名称について
Kotlin公式ブログの下記エントリにて使用されている「scope functions」が由来です。
What’s new in Standard Library M13 and M14
引用すると
Prior to M13 there were two so-called scope functions in the Standard Library: let and with. We call
them scope functions, because their only purpose is to modify scope of a function passed as the last
parameter.
要するに、letとかの関数の目的は、引数に取る関数のスコープを変更することだから「scope functions」と呼んでいると。

