- Kotlin
Kotlinスコープ関数let・apply・run・also・with使い分け
Kotlin のスコープ関数とは、あるオブジェクトを対象にして、ひとまとまりの処理をラムダの中で実行する5つの関数(let・run・with・apply・also)です。 違いは2点だけで、「ラムダの中で対象を this と呼ぶか it と呼ぶか」と「戻り値がラムダの結果か、対象そのものか」です。
この記事では、5つの関数を4つの観点で比較表にまとめ、同じ処理を5通りで書いた実行結果を並べます。さらに、「null チェックしたい→let」のように目的から選べる早見表と判定手順、入れ子にしたときの it の取り違え、apply の中で this が外側を指してしまう例、takeIf・takeUnless との組み合わせまで、すべて動かした出力つきで説明します。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。掲載した出力とエラー文は、この環境でコンパイル・実行して得たものです。
スコープ関数の早見表
| 関数 | 対象の呼び方 | 戻り値 | 拡張関数か | null 許容型に ?. で使えるか |
ひとことで言うと |
|---|---|---|---|---|---|
let |
it |
ラムダの結果 | はい | 使える(x?.let { }) |
対象を使って別の値を作る・null でないときだけ実行 |
run |
this |
ラムダの結果 | はい | 使える(x?.run { }) |
設定しつつ結果を計算する |
with |
this |
ラムダの結果 | いいえ(引数で渡す) | ?. は付けられない(中で this?. と書く) |
同じ対象への呼び出しをまとめる |
apply |
this |
対象そのもの | はい | 使える(x?.apply { }) |
対象を設定して、そのまま返す |
also |
it |
対象そのもの | はい | 使える(x?.also { }) |
対象はそのままに、ついでの処理を挟む |
「対象」は、公式ドキュメントでは context object(コンテキストオブジェクト)と呼ばれています。この記事では、x.let { } の x のことを「対象」または「レシーバ」と書きます。
目的から選ぶ早見表
| やりたいこと | 選ぶ関数 | 書き方の例 |
|---|---|---|
| null でないときだけ処理したい | let |
user?.let { println(it.name) } |
| 計算結果を一時的な変数として使いたい | let |
text.trim().let { "[$it]" } |
| オブジェクトの初期設定をしたい | apply |
User().apply { name = "Aoi" } |
| 設定しながら結果を計算したい | run |
user.run { "$name($age)" } |
| 途中でログや検証を挟みたい | also |
list.also { println(it) } |
| 同じオブジェクトのメソッドをまとめて呼びたい | with |
with(user) { println(name) } |
| 式が必要な場所で複数の文を書きたい | レシーバなしの run |
val x = run { ... } |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlinスコープ関数let・apply・run・also・with使い分け」
まず動かす:5つの関数の戻り値を並べる
次のコードを s1.kt として保存すると、そのまま実行できます。
// s1.kt
data class User(var name: String = "", var age: Int = 0)
fun main() {
val r1 = User("Aoi", 20).let { "${it.name} は ${it.age} 歳" }
val r2 = User("Aoi", 20).run { "$name は $age 歳" }
val r3 = with(User("Aoi", 20)) { "$name は $age 歳" }
val r4 = User("Aoi", 20).apply { age += 1 }
val r5 = User("Aoi", 20).also { it.age += 1 }
println("let -> $r1")
println("run -> $r2")
println("with -> $r3")
println("apply -> $r4")
println("also -> $r5")
}
kotlinc s1.kt -include-runtime -d s1.jar
java -Dstdout.encoding=UTF-8 -jar s1.jar
-Dstdout.encoding=UTF-8 は、日本語の出力を UTF-8 で表示するための指定です。
実行結果:
let -> Aoi は 20 歳
run -> Aoi は 20 歳
with -> Aoi は 20 歳
apply -> User(name=Aoi, age=21)
also -> User(name=Aoi, age=21)
let・run・with はラムダの最後の式(文字列)が戻り値になり、apply・also は対象の User がそのまま戻り値になっています。 また、let と also は it.name のように it を付けて呼び、run・with・apply は name だけで呼べています。
違いは2つの軸で整理できる
| 戻り値がラムダの結果 | 戻り値が対象そのもの | |
|---|---|---|
対象を this で呼ぶ |
run、with |
apply |
対象を it で呼ぶ |
let |
also |
軸1:this と it の違い
| 呼び方 | 書き方 | 向いている場面 | 気をつける点 |
|---|---|---|---|
this(省略できる) |
name = "Aoi" |
対象のプロパティやメソッドを続けて呼ぶ | 外側のクラスにも同じ名前があると、どちらを指すかがわかりにくい |
it(名前を変えられる) |
it.name = "Aoi" |
対象を引数として別の関数に渡す | 入れ子にすると、内側の it が外側の it を隠す |
it は { user -> ... } のように好きな名前に変えられます。this は名前を変えられません。
軸2:戻り値の違い
| 戻り値 | 関数 | チェーン(つなげて書く)での動き |
|---|---|---|
| ラムダの結果 | let、run、with |
型が変わる(User から String へ、など) |
| 対象そのもの | apply、also |
型が変わらず、同じ対象に続けて処理できる |
チェーンの途中で型が変わる様子を確かめます。
// s8.kt(前半)
data class User(var name: String = "", var age: Int = 0)
fun main() {
val length = User("Aoi", 20)
.apply { age += 1 } // User のまま
.also { println("apply のあと: $it") } // User のまま
.let { it.name } // String に変わる
.also { println("let のあと: $it") }
.run { length } // Int に変わる
println("最終結果: $length")
}
実行結果:
apply のあと: User(name=Aoi, age=21)
let のあと: Aoi
最終結果: 3
同じ処理を5通りで書いて比べる
「名前と年齢を設定して、内容を表示する」という同じ処理を、5つの関数で書きました。
// s2.kt
data class User(var name: String = "", var age: Int = 0)
// 同じ処理「名前と年齢を設定して、内容を表示する」を5通りで書く
fun main() {
val a = User().let {
it.name = "Aoi"
it.age = 20
println("let の中: $it")
it // 自分で返す必要がある
}
val b = User().run {
name = "Aoi"
age = 20
println("run の中: $this")
this // 自分で返す必要がある
}
val c = with(User()) {
name = "Aoi"
age = 20
println("with の中: $this")
this // 自分で返す必要がある
}
val d = User().apply {
name = "Aoi"
age = 20
println("apply の中: $this")
} // レシーバがそのまま返る
val e = User().also {
it.name = "Aoi"
it.age = 20
println("also の中: $it")
} // レシーバがそのまま返る
println(listOf(a, b, c, d, e).all { it == User("Aoi", 20) })
// 最後の行を書き忘れた場合の戻り値
val f = User().let { it.name = "Aoi" }
val g = User().apply { name = "Aoi" }
println("let で最後の行が代入: $f")
println("apply: $g")
}
実行結果:
let の中: User(name=Aoi, age=20)
run の中: User(name=Aoi, age=20)
with の中: User(name=Aoi, age=20)
apply の中: User(name=Aoi, age=20)
also の中: User(name=Aoi, age=20)
true
let で最後の行が代入: kotlin.Unit
apply: User(name=Aoi, age=0)
5つとも同じ結果(true)になりました。ただし、書きやすさには差があります。
| 関数 | この処理での書きやすさ | 理由 |
|---|---|---|
apply |
いちばん短い | this を省略でき、対象がそのまま返る |
also |
短い | 対象がそのまま返るが、it. を毎回書く |
run、with |
1行多い | 最後に this を書いて返す必要がある |
let |
1行多い | it. を毎回書き、最後に it を返す必要がある |
初期設定のように「設定して、その対象を使い続ける」処理は apply が向いています。 最後の2行の出力が示すとおり、let の最後の行が代入だと戻り値は kotlin.Unit(値なしを表す型)になります。対象を受け取りたいなら apply か also を選ぶと、返し忘れが起きません。
関数ごとの使い方
5つの関数の代表的な使い方を、1つのファイルにまとめました。
// s3.kt
data class User(var name: String = "", var age: Int = 0)
fun findUser(id: Int): User? = if (id == 1) User("Aoi", 20) else null
fun main() {
// let: null でないときだけ実行
findUser(1)?.let { println("let : ${it.name} が見つかった") }
findUser(2)?.let { println("この行は出力されない") }
val label = findUser(2)?.let { it.name } ?: "該当なし"
println("let + ?: -> $label")
// run: 設定と計算をまとめる
val text = findUser(1)?.run { "$name($age)" } ?: "該当なし"
println("run : $text")
// apply: 初期設定
val sb = StringBuilder().apply {
append("id=1")
append(", name=Aoi")
}
println("apply: $sb")
// also: 途中でログ
val sorted = listOf(3, 1, 2)
.also { println("also : 並べ替え前 $it") }
.sorted()
.also { println("also : 並べ替え後 $it") }
println("結果: $sorted")
// with: 同じオブジェクトの呼び出しをまとめる
val user = User("Aoi", 20)
with(user) {
println("with: 名前=$name")
println("with: 年齢=$age")
}
// 拡張でない run: 式が必要な場所で複数の文を書く
val limit = run {
val base = 10
val bonus = 5
base + bonus
}
println("run(レシーバなし): $limit")
// with に null 許容型を渡す
val maybe: User? = findUser(2)
val w = with(maybe) { this?.name ?: "with の中で this は null" }
println(w)
}
実行結果:
let : Aoi が見つかった
let + ?: -> 該当なし
run : Aoi(20)
apply: id=1, name=Aoi
also : 並べ替え前 [3, 1, 2]
also : 並べ替え後 [1, 2, 3]
結果: [1, 2, 3]
with: 名前=Aoi
with: 年齢=20
run(レシーバなし): 15
with の中で this は null
let:null でないときだけ実行する
?.let { } は、対象が null でないときだけラムダを実行します。null のときは何もせず、式全体が null になります。?:(エルビス演算子。左が null のとき右の値を使う)と組み合わせると、null のときの値も1行で書けます。
val label = findUser(2)?.let { it.name } ?: "該当なし"
if で書いたコードと並べると、一時変数が不要になることがわかります。
// s10.kt(抜粋して単体で動く形)
data class User(var name: String = "", var age: Int = 0)
fun findUser(id: Int): User? = if (id == 1) User("Aoi", 20) else null
fun main() {
// 直す前: 一時変数と if
val u1 = findUser(1)
if (u1 != null) {
println("直す前: ${u1.name}")
}
// 直した後: ?.let
findUser(1)?.let { println("直した後: ${it.name}") }
// 関数参照を渡す
findUser(1)?.name?.let(::println)
}
実行結果:
直す前: Aoi
直した後: Aoi
Aoi
ラムダの中身が関数を1つ呼ぶだけなら、let(::println) のように関数参照を渡せます。
let が役立つ場面:var プロパティの null チェック
クラスの var プロパティは、if で null チェックをしても、その先で null でない型として扱えません。
// s9_ng.kt
data class User(var name: String = "", var age: Int = 0)
class Holder {
var user: User? = User("Aoi", 20)
fun print() {
if (user != null) {
println(user.name)
}
}
}
コンパイル結果:
s9_ng.kt:7:21: error: smart cast to 'User' is impossible, because 'user' is a mutable property that could be mutated concurrently.
println(user.name)
^^^^
「チェックのあとで、ほかの処理が値を書き換えるかもしれない」という理由です。?.let を使うと、その時点の値が it に入るので、null でない型として使えます。
class Holder {
var user: User? = User("Aoi", 20)
fun print() {
user?.let { println("let なら使える: ${it.name}") }
}
}
実行結果(Holder().print()):
let なら使える: Aoi
apply:初期設定をまとめる
apply は、オブジェクトを作った直後の設定に向いています。this を省略できるので、プロパティの代入やメソッド呼び出しが並ぶだけの読みやすい形になります。
val sb = StringBuilder().apply {
append("id=1")
append(", name=Aoi")
}
戻り値は対象そのものなので、そのまま変数に入れたり、関数の戻り値にしたりできます。
also:対象はそのままに、ついでの処理を挟む
also は、メインの流れを変えずに、ログ出力や検証を挟みたいときに使います。戻り値は対象そのものなので、チェーンの途中に入れても前後の型が変わりません。
// s10.kt(抜粋して単体で動く形)
data class User(var name: String = "", var age: Int = 0)
fun register(name: String, age: Int): User =
User(name, age)
.also { require(it.age >= 0) { "年齢は0以上: ${it.age}" } }
.also { println("登録: $it") }
fun main() {
register("Aoi", 20)
try {
register("Ren", -1)
} catch (e: IllegalArgumentException) {
println("例外: ${e.message}")
}
}
実行結果:
登録: User(name=Aoi, age=20)
例外: 年齢は0以上: -1
also の行を消しても、register の戻り値は変わりません。「消しても結果が変わらない処理」を書く場所と考えると、選びやすくなります。
run:設定と計算をまとめる/式の場所に複数の文を書く
run には2つの形があります。
| 形 | 書き方 | 使いどころ |
|---|---|---|
| 拡張関数の run | user.run { "$name($age)" } |
対象のプロパティを使って結果を計算する |
| レシーバなしの run | run { ... } |
式が必要な場所に、複数の文をまとめて書く |
レシーバなしの run は、途中の変数(base や bonus)を外に残さずに値を作れます。
val limit = run {
val base = 10
val bonus = 5
base + bonus
}
with:同じ対象への呼び出しをまとめる
with は拡張関数ではなく、対象を引数として渡す関数です。「このオブジェクトについて、次のことをする」と読めるので、同じ対象のメソッドを続けて呼ぶ場面に向いています。
with(user) {
println("with: 名前=$name")
println("with: 年齢=$age")
}
null 許容型で使えるか
let・run・apply・also は拡張関数なので、?. を付ければ「null でないときだけ実行」になります。with は引数で渡すため ?. を付けられません。?. を付けずに null 許容型に使うと、次のコンパイルエラーになります。
// s4_ng.kt
data class User(var name: String = "", var age: Int = 0)
fun findUser(id: Int): User? = if (id == 1) User("Aoi", 20) else null
fun main() {
val maybe: User? = findUser(1)
with(maybe) {
println(name)
}
maybe.run { println(name) }
maybe.let { println(it.name) }
}
コンパイル結果:
s4_ng.kt:8:17: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'User?'.
println(name)
^^^^
s4_ng.kt:10:25: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'User?'.
maybe.run { println(name) }
^^^^
s4_ng.kt:11:27: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'User?'.
maybe.let { println(it.name) }
^
| 書き方 | 結果 | 直し方 |
|---|---|---|
maybe.let { it.name } |
コンパイルエラー(it が User? のまま) |
maybe?.let { it.name } |
maybe.run { name } |
コンパイルエラー(this が User? のまま) |
maybe?.run { name } |
with(maybe) { name } |
コンパイルエラー(this が User? のまま) |
maybe?.run { name } に書き換える。または中で this?.name と書く |
null かもしれない対象には、with より ?.let や ?.run が向いています。
null のときの処理も書く:?.let { } ?: run { }
null でないときと null のときの両方を書きたい場合は、?: の右にレシーバなしの run を置けます。
// s10.kt(抜粋して単体で動く形)
data class User(var name: String = "", var age: Int = 0)
fun findUser(id: Int): User? = if (id == 1) User("Aoi", 20) else null
fun main() {
for (id in 1..2) {
findUser(id)?.let { println("id=$id: ${it.name}") } ?: run { println("id=$id: 見つからない") }
}
}
実行結果:
id=1: Aoi
id=2: 見つからない
気をつける点:let の結果が null だと右側も動く
a?.let { ... } ?: b は if-else と同じではありません。let のラムダが null を返した場合も、?: の右側が実行されます。
// s11.kt
// let のラムダが null を返すと、?: の右側も動く
fun save(name: String): String? {
println("save($name) を実行")
return null // 保存後に返す値が無い想定
}
fun main() {
val name: String? = "Aoi"
name?.let { save(it) } ?: println("名前が null のときだけ出したいメッセージ")
println("--- if で書いた場合")
if (name != null) save(name) else println("名前が null のときだけ出したいメッセージ")
}
実行結果:
save(Aoi) を実行
名前が null のときだけ出したいメッセージ
--- if で書いた場合
save(Aoi) を実行
name は "Aoi" なのに、1つ目の書き方ではメッセージも出力されました。両方の場合で処理を分けたいときは、if-else で書くほうが意図どおりに動きます。
選び方の判定手順
迷ったときは、次の順番で質問に答えると1つに決まります。
flowchart TD
A[スコープ関数を使いたい] --> B{対象そのものを<br>返したい?}
B -- はい --> C{対象の設定が目的?}
C -- はい --> D[apply]
C -- いいえ ログや検証 --> E[also]
B -- いいえ 別の値を返す --> F{対象が null かも<br>しれない?}
F -- はい --> G[?.let]
F -- いいえ --> H{対象のメンバーを<br>続けて使う?}
H -- はい 結果がほしい --> I[run]
H -- はい 呼び出しをまとめる --> J[with]
H -- いいえ 引数として渡す --> K[let]
同じ内容を表にすると、次のとおりです。
| 手順 | 質問 | はい | いいえ |
|---|---|---|---|
| 1 | 対象そのものを返したい(そのまま使い続けたい)か | 手順2へ | 手順3へ |
| 2 | 目的は対象の設定か | apply |
also(ログ・検証など) |
| 3 | 対象は null かもしれないか | ?.let |
手順4へ |
| 4 | 対象のプロパティやメソッドを続けて使うか | 結果がほしいなら run、呼び出しをまとめるだけなら with |
let(別の関数に渡す・変数として使う) |
つまずきやすい点1:入れ子にすると it を取り違える
スコープ関数やラムダを入れ子にすると、内側の it が外側の it を隠します。
// s5.kt
data class Item(val name: String, val tags: List<String>)
fun main() {
val items = listOf(Item("ペン", listOf("文具", "黒")), Item("ノート", listOf("文具")))
// 入れ子の it: 内側のラムダで外側の it は見えなくなる
items.first().let {
it.tags.forEach {
println("it = $it") // この it はタグ(String)
}
}
// 名前を付けると取り違えない
items.first().let { item ->
item.tags.forEach { tag ->
println("${item.name} のタグ: $tag")
}
}
}
実行結果:
it = 文具
it = 黒
ペン のタグ: 文具
ペン のタグ: 黒
1つ目の書き方では、内側の it はタグ(String)を指しており、商品(Item)の名前を使えません。コンパイルは通るので、気づきにくい点です。入れ子にするときは item ->、tag -> のように名前を付けると、どちらも使えて読みやすくなります。
つまずきやすい点2:apply の中で this が外側を指す
apply・run・with の中では this を省略できます。そのため、対象に無い名前を書くと、外側のクラスの同じ名前のプロパティが使われます。
// s6.kt
class Button {
var label = ""
override fun toString() = "Button(label=$label)"
}
class Screen {
var label = "画面の見出し"
fun build(): Button = Button().apply {
label = "送信" // Button の label(内側の this が優先)
}
fun buildText(): StringBuilder = StringBuilder().apply {
label = "送信" // StringBuilder に label は無い → Screen の label を書き換える
}
fun buildSafe(): Button = Button().also {
it.label = "送信" // it を使えば対象がはっきりする
}
fun both(): String = Button().run {
label = "OK"
"内側=${this.label} / 外側=${this@Screen.label}"
}
}
fun main() {
val s1 = Screen()
println("build() -> ${s1.build()} / Screen.label=${s1.label}")
val s2 = Screen()
println("buildText() -> '${s2.buildText()}' / Screen.label=${s2.label}")
val s3 = Screen()
println("buildSafe() -> ${s3.buildSafe()} / Screen.label=${s3.label}")
println("both() -> ${Screen().both()}")
}
実行結果:
build() -> Button(label=送信) / Screen.label=画面の見出し
buildText() -> '' / Screen.label=送信
buildSafe() -> Button(label=送信) / Screen.label=画面の見出し
both() -> 内側=OK / 外側=画面の見出し
| 関数 | 起きたこと |
|---|---|
build() |
Button に label があるので、Button の label が「送信」になった |
buildText() |
StringBuilder に label が無いので、Screen の label が「送信」に書き換わった。StringBuilder は空のまま |
buildSafe() |
it.label と書いたので、対象がはっきりしている |
both() |
this.label は内側(Button)、this@Screen.label は外側(Screen)を指す |
buildText() はコンパイルが通り、エラーも出ません。対処は次の3つです。
| 対処 | 書き方 |
|---|---|
it で呼ぶ関数に変える |
also { it.label = ... }(対象に無い名前はコンパイルエラーになる) |
| 外側を明示する | this@Screen.label |
| 内側を明示する | this.label(対象に無い名前はコンパイルエラーになる) |
takeIf・takeUnless と組み合わせる
takeIf と takeUnless は、スコープ関数と一緒に使うことの多い標準ライブラリの関数です。
| 関数 | 条件が true のとき | 条件が false のとき |
|---|---|---|
takeIf { 条件 } |
対象そのもの | null |
takeUnless { 条件 } |
null | 対象そのもの |
戻り値が null 許容型になるので、?.let や ?: とつなげて「条件を満たすときだけ処理する」を1つの式で書けます。
// s7.kt
fun main() {
val input = "kotlin scope functions"
// takeIf: 条件を満たせば自分自身、満たさなければ null
println("takeIf : ${10.takeIf { it > 5 }} / ${3.takeIf { it > 5 }}")
// takeUnless: 条件を満たさなければ自分自身
println("takeUnless : ${10.takeUnless { it > 5 }} / ${3.takeUnless { it > 5 }}")
// takeIf + let: 見つかったときだけ処理する
for (word in listOf("scope", "java")) {
input.indexOf(word).takeIf { it >= 0 }
?.let { println("'$word' は $it 文字目から") }
?: println("'$word' は見つからない")
}
// takeUnless + ?: : 空文字なら既定値
for (name in listOf("Aoi", " ")) {
val display = name.takeUnless { it.isBlank() } ?: "名無し"
println("表示名: $display")
}
// takeIf + also + let をつなげる
val result = "42".toIntOrNull()
?.takeIf { it in 1..100 }
?.also { println("範囲内の値: $it") }
?.let { it * 2 }
println("結果: $result")
}
実行結果:
takeIf : 10 / null
takeUnless : null / 3
'scope' は 7 文字目から
'java' は見つからない
表示名: Aoi
表示名: 名無し
範囲内の値: 42
結果: 84
| 組み合わせ | 意味 | 例 |
|---|---|---|
takeIf { } ?.let { } |
条件を満たすときだけ処理する | 文字列が見つかった位置を使う |
takeUnless { } ?: 既定値 |
条件に当てはまるなら既定値にする | 空白だけの名前を「名無し」にする |
?.takeIf { } ?.also { } ?.let { } |
絞り込み → ログ → 変換 | 範囲内の数値だけ2倍にする |
indexOf は見つからないと -1 を返すので、takeIf { it >= 0 } で「見つかったときだけ」に絞っています。インデックスは 0 から数えるため、"scope" は 7 と表示されます。
let の中の return は外側の関数から抜ける
スコープ関数のラムダの中に return を書くと、ラムダではなく、外側の関数から抜けます。
// s10.kt(抜粋して単体で動く形)
data class User(var name: String = "", var age: Int = 0)
fun findUser(id: Int): User? = if (id == 1) User("Aoi", 20) else null
// let の中の return は、外側の関数から抜ける
fun greet(id: Int): String {
val user = findUser(id) ?: return "該当なし"
user.name.let {
if (it.isEmpty()) return "名前が空"
}
return "こんにちは、${user.name} さん"
}
fun main() {
println("${greet(1)} / ${greet(2)}")
}
実行結果:
こんにちは、Aoi さん / 該当なし
findUser(id) ?: return "該当なし" は、null のときに関数を早めに抜ける定番の書き方です。この形なら、以降の行で user を null でない型として使えるので、スコープ関数で囲む必要がなくなります。
読みやすさを保つチェックリスト
公式ドキュメントの「Scope functions」には、スコープ関数はコードを簡潔にできる一方、使いすぎると読みにくくなること、入れ子は避け、チェーンでは this や it が何を指すかに注意することが書かれています。次の表で確認できます。
| 確認すること | 理由 |
|---|---|
| 入れ子が2段以上になっていないか | it や this が何を指すかがわかりにくくなる |
| 入れ子にするなら it に名前を付けたか | 内側の it が外側の it を隠すため |
| apply・run・with の中で、対象に無い名前を書いていないか | 外側のクラスのプロパティが使われるため |
| 対象を返したいのに let・run を使っていないか | 最後の行が代入だと kotlin.Unit が返る |
| null のときとそうでないときで処理を分けるなら if-else にしたか | ?.let { } ?: ... は let の結果が null でも右側が動く |
| 1行で済む処理を無理にスコープ関数で囲んでいないか | user.name で足りる場面では、そのまま書くほうが読みやすい |
よくある質問
Q. let と run はどう違いますか?
A. どちらもラムダの結果を返します。違いは対象の呼び方で、let は it、run は this です。対象を別の関数の引数に渡すなら let、対象のプロパティやメソッドを続けて呼ぶなら run が書きやすくなります。
Q. apply と also はどう違いますか?
A. どちらも対象そのものを返します。apply は this で呼ぶので初期設定に、also は it で呼ぶのでログ出力や検証のような「ついでの処理」に向いています。外側のクラスと名前が重なりそうなときは、also にすると対象がはっきりします。
Q. with と run はどう使い分けますか?
A. 働きは同じで、書き方が違います。with は with(user) { } と引数で渡し、run は user.run { } と書きます。対象が null かもしれないときは ?.run、null でない対象の呼び出しをまとめるだけなら with が読みやすくなります。
Q. スコープ関数は入れ子にしてもよいですか?
A. 書くことはできますが、公式ドキュメントでは入れ子を避けることがすすめられています。内側の it が外側の it を隠したり、this が何を指すかがわかりにくくなったりするためです。入れ子にするときは item -> のように名前を付けると、読みやすさを保てます。
まとめ
- 違いは2つの軸:対象を this で呼ぶか it で呼ぶか、戻り値がラムダの結果か対象そのものか
- null でないときだけ処理 →
?.let、初期設定 →apply、途中でログや検証 →also - 設定しながら結果を計算 →
run、同じ対象の呼び出しをまとめる →with - with は拡張関数ではないので
?.を付けられない。null かもしれない対象には?.letや?.runを使う - 入れ子にすると内側の it が外側の it を隠す。名前を付けると取り違えない
- apply の中で対象に無い名前を書くと、外側のクラスのプロパティが書き換わる。also か
this@クラス名で明示する - takeIf・takeUnless は
?.letや?:とつなげると、条件つきの処理を1つの式で書ける
参考・出典
- Kotlin 公式ドキュメント「Scope functions」(関数の比較表、選び方、takeIf と takeUnless): https://kotlinlang.org/docs/scope-functions.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlinスコープ関数let・apply・run・also・with使い分け」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。