Kotlinのスコープ関数といえば、let
、run
、apply
、also
が代表的ですね。apply
はUIオブジェクトに対して複数のプロパティに値をセットするときなどに便利です。let
はnullableなオブジェクトに対して処理を行うときなどにもよく使います。
でも、ちょっといつもと違う使い方をしようとしたとき、戻り値は何だっけ?とか、run
やalso
ってどうやって使うんだっけ?となりがちです。
そこで今回は、スコープ関数の定義をちゃんと読んでみます。これができるようになれば、いちいちネットで調べなくても、Android Studioにポップアップ表示される関数定義をみるだけで使い方を確認できます。
let
それでは手始めに、let
の定義を見てみます。
public inline fun <T, R> T.let(block: (T) -> R): R
はい出ました。T
とかR
とかよく分からなくて目をそむけたくなりますが、一つ一つ確認してみます。
まずは<T, R>
の部分です。これはジェネリクスの型パラメータで、「T
とR
という2つ任意の型を使いますよ」という意味です。T
とR
は任意の型なので、例えばInt
とString
など何でもOKということです。
次にT.let
の部分です。「型.関数名」の書き方は、Kotlinの拡張関数で、既存の型(クラス)に対して関数を追加する仕組みです。ですのでここは、「T
という任意の型に対してlet
という関数を追加しますよ」という意味です。このように定義してあるおかげで、任意の型でlet
を使えるようになるんですね。
次にblock: (T) -> R
の部分です。block
は何だか意味深な言葉なので難しく考えてしまいそうになりますが、単なる引数名です。続く(T) -> R
が、引数block
の型になります。これは「T
という型を引数に取り、R
という型を返す関数」を表します。実際はラムダ式を使う場合がほとんどでしょうから、「let
は『T
という型を引数に取り、R
という型を返すラムダ式』を引数に取りますよ」という意味になります。
最後の: R
は一般的な関数の定義と同じで、戻り値を表します。つまり、「let
はR
という型を返しますよ」という意味になります。ここのR
は、let
の引数のラムダ式の戻り値と一緒ですので、ラムダ式の戻り値がそのままlet
の戻り値になります。
全体を通して確認すると、let
は、「T
という任意の型の拡張関数で、『T
を引数にとり、R
という任意の型を返すラムダ式』を引数にとり、R
を返す」ということになります。
実際の使い方で確認してみましょう。
val a: String = "10"
val b: Int = a.let {
it.toInt()
}
a
はString
、b
はInt
ですので、このlet
は「String
の拡張関数で、『String
を引数に取り、Int
を返すラムダ式』を引数に取り、Int
を返す」関数です。{ }で囲まれた部分はラムダ式です。Kotlinでは引数の最後がラムダ式の場合は( )の外に出して書くのが一般的ですが、以下のように( )の中に書いても同じ意味になります。また、ラムダ式の引数が1つの場合は暗黙の変数it
で引数を参照できますが、以下のように明示的に書いても文法的に問題はありません。
val a: String = "10"
val b: Int = a.let({it -> it.toInt()})
このように書くと、let
の引数は『String
を引数に取り、Int
を返す関数』であること、let
の戻り値はInt
であることが分かりやすいかと思います。ですがKotlin的には最初の書き方のほうが美しいですね。
Kotlinでは、ラムダに複数の式が含まれる場合は、最後の式の結果がラムダ式の戻り値になります。上の例では、toInt()
の戻り値のInt
がラムダ式の戻り値になり、そのままlet
の戻り値としてb
に代入されます。
まとめると、図のようになります。ちなみにレシーバというのは、クラスのメンバを保持しているオブジェクトのことです。ここではlet
はT
の関数なので、逆に言うとT
はlet
のレシーバということになります。
apply
続いてapply
を見てみましょう。
public inline fun <T> T.apply(block: T.() -> Unit): T
今度はジェネリクスはT
だけです。その代わり、T.() -> Unit
という謎の書式が出てきました。() -> Unit
の部分は、引数なし、戻り値なしの関数を表します。問題はT.
の部分。これは、「レシーバ付き」の関数型を表しています。この書式で定義される関数は、型T
の拡張関数になります。つまり、関数内(ラムダ内)はクラスT
の内部と同じような記述ができるということになります。ラムダ内のthis
はT
のオブジェクトを指すというとイメージしやすいかもしれません。
ということで、apply
は「T
という任意の型の拡張関数で、T
の拡張関数を引数にとり、T
そのものを返す」関数です。
実際の使い方です。
val list = mutableListOf(1, 2).apply {
add(3)
add(4)
}
今回のT
はMutableList
です。mutableListOf()
で作成したMutableList
のオブジェクトが、ラムダ式内のthis
になります。そして、apply
の戻り値はapply
のレシーバオブジェクトそのものですので、(1, 2, 3, 4)がlist
に代入されることになります。
run
次はrun
です。ここまでのlet
とapply
の内容が理解できていれば、run
は理解できます。
public inline fun <T, R> T.run(block: T.() -> R): R
run
は「T
という任意の型の拡張関数で、『R
という任意の型を返すT
の拡張関数』を引数にとり、R
を返す」関数です。
val size: Int = mutableListOf("red", "green", "blue").run {
add("yellow")
add("black")
size
}
also
最後にalso
です。also
も、これまでと同じ考え方で理解できます。
public inline fun <T> T.also(block: (T) -> Unit): T
also
は「T
という任意の型の拡張関数で、『T
を引数に取る関数』を引数にとり、T
そのものを返す」関数です。
val s: String = "hoge".also {
Log.d(TAG, "s = $it")
}
まとめ
ここまで、let
, apply
, run
, also
の定義と実際の使い方を確認してきました。一見複雑そうに見えるジェネリクスを使った拡張関数の定義ですが、一つ一つ確認するときちんと理解できると思います。そして一度理解すれば、使うたびにググらずに済むので、ぜひマスターしてください。
参考:
Scope Functions – Kotlin本家サイト