目次
- はじめに
- スコープ関数とは?
let― null チェック & 変換apply― オブジェクトの初期化・設定run― 出番は少ないが、これだけは覚えておくalso― ログ・デバッグ・副作用の挿入with― 非 null オブジェクトに対してまとめて処理- 使い分けフローチャート
- まとめ
はじめに
Kotlinのスコープ関数(let / apply / run / also / with)は便利な反面、「どれを使えばいいか迷う」こともありました。
本記事では、5つのスコープ関数の使い方と使い分けを、コード例を交えて整理します。
スコープ関数とは?
スコープ関数とは、オブジェクトに対してラムダ(コードブロック)を実行するための関数です。
たとえば次のようなコードがあるとします。
val user = User("Taro", "Taro@example.com")
user.name = "Taro Yamada"
user.isVerified = true
println(user)
スコープ関数を使うと、同じ処理をこのように書けます。
val user = User("Taro", "Taro@example.com").apply {
name = "Taro Yamada"
isVerified = true
}.also {
println(it)
}
「コードが短くなる」だけでなく、処理のまとまりと意図が伝わりやすくなるのがスコープ関数の本質的なメリットです。
何が「スコープ」なのか
スコープ関数の「スコープ」とは、ラムダブロック { } の内側のことです。ブロックの中では対象オブジェクトを this または it として参照でき、ブロックを抜けると参照は消えます。
user?.let { u ->
// このブロックの中だけで u を使える(スコープ)
textView.text = u.name
}
// ここでは u は存在しない
it を使う関数(let / also)では、ラムダ引数の名前は u の部分を任意の名前に変更できます。省略した場合はデフォルトで it になります。
// 省略すると it になる
user?.let {
textView.text = it.name
}
// 任意の名前をつけると意図が伝わりやすい
user?.let { currentUser ->
textView.text = currentUser.name
}
ネストが深くなるときや、同じ型のオブジェクトが複数登場するときは、it のままだと「どのオブジェクトか」が分かりづらくなるので、明示的に名前をつけるのがおすすめです。
一時変数がスコープの外に漏れないため、コードの見通しが良くなります。
まず全体像を把握する
スコープ関数の違いは「レシーバの参照方法」と「戻り値」の2軸だけです。
| 関数 | レシーバの参照 | 戻り値 | 拡張関数? |
|---|---|---|---|
let |
it |
ラムダの結果 | ✅ |
run |
this |
ラムダの結果 | ✅ |
run |
- | ラムダの結果 | ❌(コンテキストオブジェクトなし) |
apply |
this |
レシーバ自身 | ✅ |
also |
it |
レシーバ自身 | ✅ |
with |
this |
ラムダの結果 | ❌(トップレベル関数) |
選ぶときの2ステップ:
- 戻り値は何が欲しいか?
-
変換した別の値 →
let/run/with -
自分自身(チェーン用) →
apply/also
-
変換した別の値 →
- レシーバをどう参照したいか?
-
itで受けて明示的に使いたい →let/also -
this省略でメンバに直アクセスしたい →run/apply/with
-
let ― null チェック & 変換
特徴: it で受け取り、ラムダの結果を返す。
シーン① nullable な値の安全処理
Android で最も使う場面。?.let で null の場合をスキップできます。
// Before
val name = user?.name
if (name != null) {
textView.text = name
}
// After
user?.name?.let { name ->
textView.text = name
}
複数プロパティへのアクセスもまとめられます。
user?.let { u ->
nameTextView.text = u.name
emailTextView.text = u.email
profileImageView.load(u.avatarUrl)
}
シーン② 変換(別の値を得る)
ラムダの最後の式が戻り値になるので、「変換」の意図が伝わりやすいです。
val uiState = apiResponse?.let { response ->
UserUiState(
name = response.name,
email = response.email,
)
} ?: UserUiState.empty()
val message = intent.getStringExtra("key")?.let { raw ->
"受信: $raw"
} ?: "データなし"
シーン③ スコープを限定して一時変数を減らす
// raw をスコープ外に漏らさない
val label = getLabel().let { raw ->
raw.trim().uppercase()
}
apply ― オブジェクトの初期化・設定
特徴: this でアクセスし、レシーバ自身を返す。Builder的な使い方に最適。
シーン① View の初期化
// Before
val button = Button(context)
button.text = "送信"
button.isEnabled = false
button.setBackgroundColor(Color.BLUE)
// After
val button = Button(context).apply {
text = "送信"
isEnabled = false
setBackgroundColor(Color.BLUE)
}
シーン② Builder パターンの代替
Java ライブラリの Builder に条件分岐を挟むときに便利です。
val request = Request.Builder()
.url(endpoint)
.apply {
if (requiresAuth) {
addHeader("Authorization", "Bearer $token")
}
}
.build()
シーン③ SharedPreferences の書き込み
prefs.edit().apply {
putString("user_id", userId)
putBoolean("is_logged_in", true)
apply() // ← SharedPreferences の apply()
}
注意:
apply { apply() }と見えてしまうため、混乱する場合はcommit()を使うか、変数に受け出す方が可読性が上がります。commit()は同期的にファイルへ書き出しを行っているためメインスレッドで実行することは注意が必要です。
run ― 出番は少ないが、これだけは覚えておく
特徴: this でアクセスし、ラムダの結果を返す。
| 関数 | レシーバ | 戻り値 |
|---|---|---|
let |
it |
ラムダの結果 |
apply |
this |
レシーバ自身 |
run |
this |
ラムダの結果 |
run は let と戻り値が同じ(ラムダの結果)なので、let に引数名をつけて書けば run と同等のことができます。実際、現場では let を使う頻度のほうが多いと思うので、あまり使う場面は多くなさそうな所感です。
シーン① null チェックして変換まで行う
let は it で参照するため、プロパティが多いと it.name, it.email と都度 it. が必要です。run は this 省略できるのでより自然に書けます。
val uiState = user?.run {
// this = user。プロパティに直アクセスできる
UserUiState(
displayName = "$lastName $firstName",
email = email,
isVerified = verifiedAt != null,
)
} ?: UserUiState.empty()
シーン② run が唯一の選択肢になるシーン
run にしかできないことが1つあります。レシーバなし(オブジェクトを伴わない)で使う形です。
val statusText = run {
// 一時変数は run の中だけに閉じる
val suffix = when (user.status) {
Status.ACTIVE -> "(稼働中)"
Status.SUSPENDED -> "(停止中)"
else -> ""
}
"${user.name}$suffix" // この値が statusText に入る
}
これは let では書けません(let は拡張関数なので必ずレシーバが必要)。
コードブロックの結果を変数に代入するといった場合に使用できます。
初期化などのロジックが長くなる場合は検討しても良さそうです。
also ― ログ・デバッグ・副作用の挿入
特徴: it で受け取り、レシーバ自身を返す。「メインの処理は変えずに、途中で何か追加したい」ときに使います。
シーン① デバッグログの差し込み
処理チェーンを壊さずにログを挟めるのが最大のメリットです。
val user = repository.getUser(id)
.also { Log.d("TAG", "取得したユーザー: $it") }
シーン② 副作用(通知・アナリティクス)の挿入
also はレシーバ自身をそのまま返すので、ブロックの中で何をしても元の値が変わらず呼び出し元に届きます。これを利用して、メインの処理の流れを変えずに「横で何かしたい」ときに使います。
fun login(credentials: Credentials): Result<User> {
return authRepository.login(credentials)
.also { result ->
if (result.isSuccess) {
analytics.track("login_success")
}
}
}
also を使わずに書くと以下と同じ意味です。やっていることは変わらず、一時変数を作らずチェーンで書けるのがメリットです。
fun login(credentials: Credentials): Result<User> {
val result = authRepository.login(credentials)
if (result.isSuccess) {
analytics.track("login_success")
}
return result // result を変えずそのまま返している
}
ここでの analytics.track() のように、関数の本来の目的(ログイン処理)とは別に発生する処理を「副作用」と呼びます。also は「値の流れは変えたくないが、横で通知・ログ・計測だけしたい」用途に向いています。
with ― 非 null オブジェクトに対してまとめて処理
特徴: 拡張関数ではなくトップレベル関数。with(obj) { ... } の形で使い、ラムダの結果を返す。
大半のケースは apply(戻り値不要)や run(戻り値あり)で代替できます。
run との大きな違いは、トップレベル関数のためメソッドチェーンや ?. が使えない点です。
そのため nullable なオブジェクトには使えず、非 null が確定している場面に限られます。
ただ with(object) { } の形は見かける可能性があるため、
読み書きできるようにしておくと良さそうです。
with には戻り値を使うかどうかで2つの使い方があります。
パターンA:戻り値を使わない(操作をまとめる)
ブロック内でオブジェクトに対する操作をまとめて書くだけで、結果は受け取りません。
// binding は ViewBinding(レイアウトの View を型安全に参照する仕組み)のインスタンス
with(binding) {
nameTextView.text = user.name
emailTextView.text = user.email
submitButton.isEnabled = user.isVerified
}
// 戻り値は捨てている(Unit)
binding は非 null が保証されているため、with でプロパティへのアクセスをひとまとめにできます。
パターンB:戻り値を使う(値の計算)
ブロックの最後の式が戻り値になる性質を活かして、オブジェクトのプロパティをもとに新しい値を作れます。
val summary = with(order) {
// ブロックの最後の式が summary に入る
"${itemCount}点 合計: ${totalPrice}円 (${discountRate}%OFF)"
}
使い分けフローチャート
オブジェクトに対して処理したい
│
戻り値は何?
┌──────┴──────┐
レシーバ自身 別の値・変換結果
│ │
副作用のみ? プロパティに
├─ YES → also 直アクセスしたい?
└─ NO → apply ├─ YES → run / with
└─ NO → let
※ null チェックが必要 → let / run に ?. をつける
※ 非 null のオブジェクトをまとめて操作 → with が読みやすい
まとめ
| やりたいこと | 使う関数 |
|---|---|
| null チェックして安全に処理 | ?.let |
null チェック + this 省略で変換 |
?.run |
| オブジェクトを初期化・設定してそのまま返す | apply |
| ログ・副作用を処理チェーンに差し込む | also |
| 非 null 確定オブジェクトをまとめて操作 | with |
スコープ関数は「どれでも動く」ことが多いですが、適切な関数を選ぶことがコードの意図を伝えることにつながります。
ただ、入れ子にしたり、いくつも連鎖させると可読性が低下してしまう可能性があるためそこは要注意かと思います。