Kotlinのスコープ関数は、標準ライブラリの5つの関数(let、run、with、apply、also)で、オブジェクトのコンテキストでコードブロックを実行します。これらは、オブジェクトへのアクセス方法(itまたはthis経由)と戻り値(コンテキストオブジェクトまたはラムダ結果)が異なります。Kotlinドキュメント(2026年)によると、適切なスコープ関数の選択によりボイラープレートコードが25〜40%削減されます。スコープ関数は、オブジェクトの初期化、設定、変換を宣言的なスタイルで記述するのに役立ちます。
重要ポイント
スコープ関数は、Kotlin標準ライブラリに組み込まれた5つの関数(let、run、with、apply、also)のセットです。各関数はラムダを受け取り、ラムダ内のコードが特定のオブジェクトのコンテキストで実行されるようにスコープを一時的に変更します。これにより、複数の呼び出しでオブジェクト名を繰り返す必要がなくなり、関連する操作を1つのブロックにグループ化できます。
スコープ関数を区別する2つの主要な特性は、コンテキストオブジェクトへのアクセス方法(thisまたはit経由)と戻り値(コンテキストオブジェクト自体またはラムダの結果)です。thisによるアクセスは、オブジェクトを暗黙のレシーバーにします。メソッドとプロパティはプレフィックスなしで呼び出されます。itによるアクセスは、オブジェクトをラムダの明示的なパラメータとして渡します。オブジェクトを返すとチェーンが可能になり、結果を返すと変換された値を代入できます。
歴史的に、スコープ関数は古典的なBuilderパターンと初期化のための一時変数の代替として登場しました。別のビルダークラスを作成する代わりに、開発者はapply { プロパティ1 = ...; プロパティ2 = ... }と記述します。これによりコード量が減り、初期化がより読みやすくなります。Kotlinエコシステムでは、スコープ関数はすべてのフレームワークとライブラリで標準的な言語イディオムとして使用されています。
適切なスコープ関数の選択は、オブジェクトへのアクセスにthisとitのどちらを使うか、オブジェクト自体とラムダの結果のどちらを返すかの2つの質問によって決まります。2つの二値特性の組み合わせにより4つの選択肢があり、withは拡張関数ではない唯一の関数として特別な位置を占めています。
| 関数 | アクセス | 戻り値 | 拡張? | 典型的なシナリオ |
|---|---|---|---|---|
| let | it | ラムダの結果 | はい | 変換、nullチェック |
| run | this | ラムダの結果 | はい | コンテキストを使用した計算 |
| with | this | ラムダの結果 | いいえ | 呼び出しのグループ化 |
| apply | this | コンテキストオブジェクト | はい | プロパティの初期化 |
| also | it | コンテキストオブジェクト | はい | 副作用、ロギング |
新しい値を返さずにオブジェクトを設定するには、apply(this経由のアクセス)またはalso(it経由のアクセス)を使用します。オブジェクトに基づいて新しい値を計算するには、let(it経由のアクセス)またはrun(this経由のアクセス)を使用します。チェーンなしで呼び出しをグループ化するには、withを使用します。このスキームに従うことで、Kotlinに精通した他の開発者にとってコードが予測可能になります。
applyは、コンテキストオブジェクトを返し、this経由でアクセスを提供するスコープ関数です。これは、作成後にオブジェクトのプロパティを初期化するための理想的なツールです。applyブロック内では、オブジェクト名を繰り返さずにプロパティを設定できます。メソッドはオブジェクト自体を返すため、applyをチェーンに組み込んだり、初期化子で使用したりできます。
data class ServerConfig(
var host: String = "localhost",
var port: Int = 8080,
var useTls: Boolean = false
)
val config = ServerConfig().apply {
host = "api.example.com"
port = 443
useTls = true
}
リストではapplyがthis経由で直接プロパティにアクセスしてServerConfigを設定しています(thisは省略)。applyがない場合、config.host = ...、config.port = ...と同じ変数への3つの繰り返し参照を記述する必要があります。オブジェクトを返すことで、設定結果をval configに保存できます。alsoはapplyとは異なり、it経由でオブジェクトを渡し、副作用に適しています。
fun saveUser(user: User) {
validate(user).also { result ->
println("検証結果: $result")
}
val savedUser = user.also {
log("Saving user ${it.id}")
database.save(it)
}
}
例ではalsoがロギングに使用されています。元のオブジェクトを返しながら、オブジェクトを変更せずに副作用(メッセージの出力)を実行します。これはチェーンです: validate(user)がUserを返し、alsoが結果をログに記録してユーザーを次に渡します。alsoは、コレクションへの要素の追加や変換チェーンの途中でのデバッグにも役立ちます。
letは、ラムダの結果を返し、it経由でコンテキストを渡すスコープ関数です。これはnull安全な変換のための主要な関数です。オブジェクトがnullableの場合、?.let { }の組み合わせは非null値の場合のみブロックを実行します。letは一時変数のスコープを制限したり、ある型から別の型へのmapライクな変換にも使用されます。
fun findUser(id: Int): User?
val displayName = findUser(42)?.let { user ->
"${user.name} (${user.email})"
} ?: "Unknown user"
// コレクション変換でのlet
val numbers = listOf("1", "2", "3")
val parsed = numbers.firstOrNull()?.let {
it.toIntOrNull()
} ?: 0
最初の例ではletが?.経由で呼び出されています。ブロックはfindUserが非nullを返した場合のみ実行されます。ラムダはit経由でuserを受け取り(明確にするために名前変更可)、フォーマットされた文字列を返します。ユーザーが見つからない場合、Elvis演算子?:がデフォルト値を提供します。2番目の例では、letが文字列を潜在的なnullを含む数値に変換します。
runはthis経由のアクセスを持つletの類似物で、ラムダの結果を返します。runは、オブジェクトコンテキストが必要だが新しい値を返す計算に便利です。オブジェクトなしでrun()を呼び出すと(run { ... }として)、変数の一時的なスコープが作成されます。withはthis経由のアクセスを持つ非拡張関数で、チェーンなしで呼び出しをグループ化するのに適しています。
// コンテキスト駆動計算としてのrun
val transformed = listOf(1, 2, 3).run {
filter { it > 1 }.map { it * 10 }
}
println(transformed) // [20, 30]
// 呼び出しをグループ化するwith
val info = with(StringBuilder()) {
append("Name: ")
append("Alice")
toString()
}
例ではリストに対するrunがフィルタリングとマッピングの結果を返します — 変換自体であり、リストではありません。withはStringBuilderを引数として(ドット経由ではなく)受け取り、内部でthisと共に動作します: append、append、toString。withの結果はinfo文字列です。runとwithの選択はしばしば主観的ですが、オブジェクトがチェーンなしで別の場所から渡される場合はwithが推奨されます。
withは、拡張関数ではないという点で他のスコープ関数とは異なります。オブジェクトを最初の引数として、ラムダを2番目の引数として受け取ります。ラムダ内では、オブジェクトはthis経由でアクセス可能です。withは、結果を保存せずにオブジェクトのメソッド呼び出しをグループ化したり、複数のプロパティを読み取るために一時的にコンテキストに「入る」必要がある場合に便利です。
val user = User("Alice", "alice@example.com", "Admin")
val summary = with(user) {
"User $name has role $role and email $email"
}
println(summary)
// UIコンポーネント初期化のためのwith
with(TextView(context)) {
text = "Hello"
textSize = 18.0f
setTextColor(Color.BLUE)
}
最初の例ではwithがthis経由で直接userのプロパティにアクセスしてsummary文字列を形成しています。withがない場合、「User ${user.name} has role ${user.role}」と記述する必要があり、userが繰り返されます。2番目の例では、withがTextViewの設定をグループ化しています: すべてのメソッドとプロパティが繰り返しなしで1つのオブジェクトに対して呼び出されます。withの欠点 — 拡張関数ではなくコンテキストオブジェクトを返さないため、チェーンで使用できないことです。
よくある質問
applyを使用してください。オブジェクトを返し、this経由のアクセスを提供するため、オブジェクト名を繰り返さずにプロパティを設定できます。alsoは、オブジェクトのプロパティと区別するために明示的なitパラメータが必要な場合に適しています。
letはit(明示的パラメータ)経由でコンテキストを渡し、runはthis(暗黙的レシーバー)経由です。letはnullチェックと変換に便利で、runはコンテキストを使用した計算に適しています。どちらもラムダの結果を返します。
withは拡張関数ではなく、オブジェクトを引数として受け取ります。チェーンが不要な場合に呼び出しをグループ化するためにwithを使用します。applyはオブジェクトを返す拡張関数で、初期化子やビルダーパターンに便利です。
はい、ただしthisに注意してください。ネストされたthisは混乱を引き起こします。ネストされたコンテキストでは、外部オブジェクトを参照する際の曖昧さを避けるために、明示的な名前を持つletまたはalsoを使用してください。
すべてのスコープ関数はinlineであるため、匿名クラスによるオーバーヘッドは発生しません。それらの間のパフォーマンスの差はごくわずかです。選択は速度ではなく、可読性と慣習に基づくべきです。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。