Kotlinのスコープ関数の定義を読み解く

※本記事はQiitaに投稿していた記事のexportです(元投稿日: 2018/8/13)

Kotlinのスコープ関数はlet, with, run, apply, alsoがあります。 public inline fun <T, R> T.let(block: (T) -> R): R = block(this)などのそれぞれの定義を読んでもすぐ頭に入ってこないので、読み方を解説してみます。自分用チートシートです。

前提知識

拡張関数

高階関数

インライン関数

ジェネリクス

let

定義

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

f:id:Gateau:20200328110514p:plain

Calls the specified function [block] with this value as its argument and returns its result.

  • 自分自身Tを引数としてblock関数を実行し、その結果を返します。
  • Kotlin イン・アクション P.189

使用例

fun sendEmail(email: String): Boolean { /* ... */ }
val result = email?.let { sendEmail(it) } // it = email

安全呼び出し演算子と使えば、emailがnullでないときだけ、emailを引数にとってsendEmailを実行する、といった処理を完結に書けます。letを使わない場合はこのようにnullチェックが必要になり冗長です。

var result = false
if (email != null) { result = sendEmail(email) }

英語としてわかりやすい例

  • 英語の文法的には、「let + <目的語> + <動詞>」となり、「<目的語>に<動詞>をさせる」という意味です。
val s = "hoge".let { it.toUpperCase() } // s = "HOGE"

ただこの例だと後述のrunを使った方が "hoge".run { toUpperCase() } とスッキリします。対象オブジェクトを引数にしたいときに使う方が良さそうです。

使い時

  • if (hoge != null) ...のような記述をしたくなったとき(これが圧倒的に多い)
  • 対象オブジェクトを引数にして、まとめて処理を行いたいとき
  • thisを変えたくないとき(関数内でcontextを使った処理があるなど。this@MainActivityのような指定も可能ではある)
// Activity内で
val s = "hoge".let {
    Toast.makeText(this, it, Toast.LENGTH_LONG).show();
    it.toUpperCase()
}

with

定義

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

f:id:Gateau:20200328110608p:plain

Calls the specified function [block] with the given [receiver] as its receiver and returns its result.

  • 第一引数に任意の型Tを取り、そのTをレシーバとして第二引数を実行します。
  • 同じオブジェクトに対してその名前を繰り返すことなく複数の操作を出来る関数です。
  • Kotlin イン・アクション P.169

使用例

val numbers = with(StringBuilder()) {
    append("Numbers: ")
    for (i in 1..3) append(i)
    toString()                  // 全ての行でthisを省略出来て読みやすい
}
println(numbers) // Numbers: 123

英語としてわかりやすい例

  • withは前置詞で、後ろに名詞(句・節)か動名詞が来ます。引数として渡すので名詞が良いでしょう。〜とともに、〜を使って、などの意味があります。
val love = with(you){ date(me) }

// letを使って同じ処理を書く場合
val love = you.let { it.date(me) }

letを使っても同じ書き方は出来ますが、英語の通りとしてはwithを使った方が読みやすくなります。

使い時

  • 同じオブジェクトに対して繰り返し操作したいとき
  • 返り値が必要ないときは apply と同じで、どちらでも使える
  • あまり使われてはいない模様

run

定義

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

f:id:Gateau:20200328110643p:plain

Calls the specified function [block] with this value as its receiver and returns its result.

  • 自分自身Tをレシーバとする関数を実行し、その結果を返します。
  • Kotlin イン・アクションには出てきませんでした。

使用例

val numbers = StringBuilder().run {
    append("Numbers: ")
    for (i in 1..3) append(i)
    toString()
}
println(numbers) // Numbers: 123
  • withの例はrunでも同じように書けますね。

英語としてわかりやすい例

  • 使いどころが限定されていてあまり浮かびません…

使い時

  • 人に聞いてみても、あまり使ってないと言われることが多いです。
  • エルビス演算子と組み合わせて使うことがあるようです。

apply

定義

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

f:id:Gateau:20200328110705p:plain

Calls the specified function [block] with this value as its receiver and returns this value.

  • 自分自身Tをレシーバとする関数を実行し、実行後の自分自身を返します。
  • Kotlin イン・アクション P.171

使用例

withと似た動きをします。異なるのは、applyを実行した返り値はthisなのでStringBuilderであるため、Stringに変換するtoString()が必要な点です。

val numbers = StringBuilder().apply {
    append("Numbers: ")
    for (i in 1..3) append(i)
}.toString()
println(numbers) // Numbers: 123

英語としてわかりやすい例

  • applyは適用する・適用されるという意味です。設定画面に出てくるapplyのイメージで良さそう。
// Activityで
supportActionBar?.apply {
    setHomeButtonEnabled(true)
    setDisplayHomeAsUpEnabled(true)
}

使い時

  • ローカル変数を書かずにオブジェクトの状態を変更したいとき。上記「英語としてわかりやすい例」がそれです。
  • オブジェクトのインスタンスを生成し、すぐにいくつかのプロパティを初期化する必要があるとき。
val intent = Intent(this, FooActivity::class.java).apply {
    putExtra("id", user.id)
    putExtra("name", user.name)
}

also

定義

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

f:id:Gateau:20200328110731p:plain

Calls the specified function [block] with this value as its argument and returns this value.

  • 自分自身Tを引数とする関数を実行し、実行後の自分自身を返します。
  • Kotlin イン・アクションには出てきませんでした。

使用例

val button = Button(context).also {
    it.text = "hello"
}

applyの方がわかりやすくスッキリするようにも見えるのですが、Kotlinアンチパターンで注意事項が書かれていました。

// 最初にこういったコードの内は大丈夫なんですが…
val button = Button(context).apply {
    text = "hello"
}
val text = "" // 後でこのコードが追加されると
val button = Button(context).apply {
    text = "hello"
}
button.text // "hello"にならない

ということで、applyを禁止してalsoとする場合もあるようです。

使い時

  • applyと同じようにまとめて設定を行いたいときで、thisを変えたくないとき

自分の使用状況

let と apply をよく使っています。逆に他はほとんど使っていません。 Kotlinアンチパターンの例のように、わかりやすさのために使うスコープ関数の種類を絞るのも有りだと思いました。