Kotlinのスコープ関数の定義をちゃんと読む

Kotlinのスコープ関数といえば、letrunapplyalsoが代表的ですね。applyはUIオブジェクトに対して複数のプロパティに値をセットするときなどに便利です。letはnullableなオブジェクトに対して処理を行うときなどにもよく使います。

でも、ちょっといつもと違う使い方をしようとしたとき、戻り値は何だっけ?とか、runalsoってどうやって使うんだっけ?となりがちです。

そこで今回は、スコープ関数の定義をちゃんと読んでみます。これができるようになれば、いちいちネットで調べなくても、Android Studioにポップアップ表示される関数定義をみるだけで使い方を確認できます。

let

それでは手始めに、letの定義を見てみます。

public inline fun <T, R> T.let(block: (T) -> R): R

はい出ました。TとかRとかよく分からなくて目をそむけたくなりますが、一つ一つ確認してみます。

まずは<T, R>の部分です。これはジェネリクスの型パラメータで、「TRという2つ任意の型を使いますよ」という意味です。TRは任意の型なので、例えばIntStringなど何でもOKということです。

次にT.letの部分です。「型.関数名」の書き方は、Kotlinの拡張関数で、既存の型(クラス)に対して関数を追加する仕組みです。ですのでここは、「Tという任意の型に対してletという関数を追加しますよ」という意味です。このように定義してあるおかげで、任意の型でletを使えるようになるんですね。

次にblock: (T) -> Rの部分です。blockは何だか意味深な言葉なので難しく考えてしまいそうになりますが、単なる引数名です。続く(T) -> Rが、引数blockの型になります。これは「Tという型を引数に取り、Rという型を返す関数」を表します。実際はラムダ式を使う場合がほとんどでしょうから、「letは『Tという型を引数に取り、Rという型を返すラムダ式』を引数に取りますよ」という意味になります。

最後の: Rは一般的な関数の定義と同じで、戻り値を表します。つまり、「letRという型を返しますよ」という意味になります。ここのRは、letの引数のラムダ式の戻り値と一緒ですので、ラムダ式の戻り値がそのままletの戻り値になります。

全体を通して確認すると、letは、「Tという任意の型の拡張関数で、『Tを引数にとり、Rという任意の型を返すラムダ式』を引数にとり、Rを返す」ということになります。

実際の使い方で確認してみましょう。

val a: String = "10"
val b: Int = a.let {
    it.toInt()
}

aStringbIntですので、この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に代入されます。

まとめると、図のようになります。ちなみにレシーバというのは、クラスのメンバを保持しているオブジェクトのことです。ここではletTの関数なので、逆に言うとTletのレシーバということになります。

letの構造

apply

続いてapplyを見てみましょう。

public inline fun <T> T.apply(block: T.() -> Unit): T

今度はジェネリクスはTだけです。その代わり、T.() -> Unitという謎の書式が出てきました。() -> Unitの部分は、引数なし、戻り値なしの関数を表します。問題はT.の部分。これは、「レシーバ付き」の関数型を表しています。この書式で定義される関数は、型Tの拡張関数になります。つまり、関数内(ラムダ内)はクラスTの内部と同じような記述ができるということになります。ラムダ内のthisTのオブジェクトを指すというとイメージしやすいかもしれません。

ということで、applyは「Tという任意の型の拡張関数で、Tの拡張関数を引数にとり、Tそのものを返す」関数です。

実際の使い方です。

val list = mutableListOf(1, 2).apply {
    add(3)
    add(4)
}

今回のTMutableListです。mutableListOf()で作成したMutableListのオブジェクトが、ラムダ式内のthisになります。そして、applyの戻り値はapplyのレシーバオブジェクトそのものですので、(1, 2, 3, 4)がlistに代入されることになります。

run

次はrunです。ここまでのletapplyの内容が理解できていれば、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本家サイト