結論(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つのメソッドの役割と似ています。
map
flatMap
ifPresent
例えば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」と呼んでいると。