[Android] Kotlinスコープ関数 let / run / with / apply / also の使い分け ― 迷わない判断フローチャート付き

letrunwithapplyalso ―― Kotlinを書いていると、この5つのスコープ関数のどれを使えばいいか迷ったことはありませんか? どれも「オブジェクトを受け取ってブロックを実行する」という点では似ていて、正直どれでも動いてしまう場面も多いです。だからこそ、なんとなくで選んでしまいがちです。

この記事では、5つのスコープ関数をたった2つの軸で整理し、「この場面ならこれ」と迷わず選べるようになることをゴールにします。最後には判断フローチャートと、Android実装でのよくある使い方、ハマりやすい落とし穴もまとめました。

スポンサーリンク

スコープ関数とは何か

スコープ関数は、あるオブジェクトに対する処理を「一時的なスコープ(ブロック)」の中でまとめて書くための関数です。標準ライブラリに用意されていて、次のように使います。

// スコープ関数を使わない場合
val user = User()
user.name = "Taro"
user.age = 30
user.email = "taro@example.com"
userRepository.save(user)
 
// apply を使うと、user を繰り返し書かずに済む
val user = User().apply {
    name = "Taro"
    age = 30
    email = "taro@example.com"
}
userRepository.save(user)

見た目がスッキりするだけでなく、「この user に対する初期化はここでまとまっている」という意図が伝わりやすくなります。ただし、5つのどれを使うかでブロックの書き方戻り値が変わるため、そこを理解しておく必要があります。

使い分けの鍵は「2つの軸」だけ

5つを丸暗記する必要はありません。次の2つの軸で分類すると、一気に見通しがよくなります。

軸1:オブジェクトの参照方法(this か it か)

ブロックの中でオブジェクトをどう参照するかで2グループに分かれます。

  • this(レシーバ)で参照runwithapply。プロパティやメソッドを this. 省略でそのまま呼べる。
  • it(引数)で参照letalsoit で参照し、名前を付け替えることもできる。
// this で参照(apply): name を直接書ける
val user = User().apply {
    name = "Taro"   // this.name の this を省略
}
 
// it で参照(let): it 経由でアクセス
val length = user.name.let {
    it.trim().length
}

軸2:戻り値(ラムダの結果か、オブジェクト自身か)

ブロックが何を返すかで、さらに2グループに分かれます。

  • ラムダの結果を返すletrunwith。ブロックの最後の式が戻り値になる。
  • オブジェクト自身を返すapplyalso。ブロックの結果に関わらず、元のオブジェクトがそのまま返る。
// ラムダの結果を返す(let): length が result に入る
val result: Int = user.name.let { it.length }
 
// オブジェクト自身を返す(also): user がそのまま returned
val sameUser: User = user.also { println("id = ${it.id}") }

この2軸を表にすると、5つの関係が一目で分かります。

  • let : it で参照 / ラムダの結果を返す
  • run : this で参照 / ラムダの結果を返す
  • with: this で参照 / ラムダの結果を返す(非拡張関数。引数で渡す)
  • apply: this で参照 / オブジェクト自身を返す
  • also: it で参照 / オブジェクト自身を返す

それぞれの得意技とコード例

let ― nullチェックとスコープ限定

let の一番の使いどころは、?.let による null 安全な処理です。nullable な値が null でないときだけブロックを実行できます。

// nickname が null でないときだけ表示を更新する
fun updateLabel(nickname: String?) {
    nickname?.let {
        labelView.text = it
        labelView.visibility = View.VISIBLE
    }
    // nickname が null のときはブロックごとスキップされる
}

また、ブロック内でしか使わない一時変数のスコープを狭く保つ用途にも向いています。

run ― 設定と結果の計算をまとめる

runthis で参照しつつラムダの結果を返すので、「オブジェクトを操作した上で、何か値を計算して返す」場面に向きます。

// 設定を組み立てて、最終的に生成した Config を返す
val config = ServerConfig().run {
    host = "api.example.com"
    port = 443
    useTls = true
    build()   // ← この戻り値が config になる
}

with ― 同じオブジェクトへの連続操作

with は唯一の非拡張関数で、対象を引数として渡します。すでにあるオブジェクトに対して複数の操作をまとめたいときに読みやすくなります。

// canvas に対する複数の描画操作をまとめる
val summary = with(reportBuilder) {
    addTitle("月次レポート")
    addSection("概要")
    addSection("詳細")
    toString()   // ← 戻り値が summary になる
}

apply ― オブジェクトの初期化・設定

apply はオブジェクト自身を返すので、生成と同時に設定してそのまま変数に代入する、という定番パターンにぴったりです。

// Intent を生成しつつ設定して、そのまま渡す
val intent = Intent(context, DetailActivity::class.java).apply {
    putExtra("item_id", itemId)
    putExtra("from", "list")
    addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
startActivity(intent)

also ― 副作用(ログ・検証)の差し込み

also もオブジェクト自身を返します。it で参照するので、「本流の処理を邪魔せずに、ログ出力や検証などの副作用を挟む」用途に向いています。

// 生成した user をログに出しつつ、そのまま返す
fun createUser(name: String): User {
    return User(name = name)
        .also { Log.d("UserFactory", "created: ${it.name}") }
}

迷わない判断フローチャート

実際に「どれを使うか」で迷ったら、次の順で考えると自然に絞り込めます。

  1. 戻り値は「オブジェクト自身」が欲しい?
    • YES → 初期化・設定なら apply、ログや検証など副作用の差し込みなら also
    • NO(ラムダの結果が欲しい) → 次へ
  2. 対象が nullable で、null でないときだけ処理したい?
    • YES → ?.let
    • NO → 次へ
  3. 拡張関数として obj.xxx { } の形で書きたい?
    • YES → run
    • NO(with(obj) { } の形が読みやすい) → with

ざっくり覚えるなら「設定は apply、ログは also、nullチェックは let」の3つを押さえるだけで、実務の大半はカバーできます。runwith は「結果も返したいとき」に思い出せば十分です。

ハマりやすい落とし穴

ネストして this / it が混乱する

スコープ関数をネストすると、どの thisit がどのオブジェクトを指すのか分からなくなります。特に apply の中で apply を重ねると、内側の this が外側を隠してしまいます。

// 悪い例:this が二重になって読みにくい
outer.apply {
    inner.apply {
        // ここの this は inner。outer のプロパティと混同しやすい
        value = 10
    }
}
 
// 改善:内側は it で参照する also に変えて区別する
outer.apply {
    inner.also {
        it.value = 10   // it = inner だと一目で分かる
    }
}

ネストが必要になったら、片方を it 参照の関数(let / also)にすると、どちらのオブジェクトか判別しやすくなります。

?.let の中の return に注意

?.let のブロック内で単に return と書くと、それは let のブロックからではなく外側の関数から抜けることになります。ブロックだけを抜けたいときはラベル付き return を使います。

fun process(value: String?) {
    value?.let {
        if (it.isBlank()) return@let   // let ブロックだけを抜ける
        doSomething(it)
    }
    // return@let ならここに処理が続く
}

「動くから」で apply と also を混ぜない

applyalso はどちらもオブジェクト自身を返すため入れ替えても動いてしまいますが、意図が変わります。設定・初期化なら apply、副作用(ログ・検証・登録)なら also と役割で使い分けると、コードを読む人に意図が伝わります。統一しておくとレビューもしやすくなります。

まとめ

5つのスコープ関数は、「this か it か」「ラムダの結果かオブジェクト自身か」という2軸で整理すれば、丸暗記しなくても選べるようになります。実務では次の3つを軸に覚えておけば十分です。

  • apply:生成と同時の初期化・設定
  • also:ログや検証など副作用の差し込み
  • let?.let による null 安全な処理

残りの runwith は「操作した上で結果も返したい」ときに思い出せばOKです。迷ったら「戻り値に何が欲しいか」から考えるのが、いちばん早く正解にたどり着くコツです。まずは既存コードの notify 直前の初期化処理などを apply に置き換えるところから、少しずつ手になじませてみてください。

コメント

タイトルとURLをコピーしました