Kotlinのletは、レシーバオブジェクトをラムダ引数として受け取り、そのラムダの評価結果を返す拡張関数です。
定義は次の通りです。
inline fun <T, R> T.let(block: (T) -> R): R
この定義から分かることは、主に次の2点です。
letは、呼び出し元のオブジェクトをblockに渡すlet自身の返り値は、blockの最後の式の評価結果になる
つまりletは、「ある値を受け取って、その値を使った処理を行い、処理結果を返す」関数です。
基本の動き
たとえば次のコードを見てください。
val result = "Kotlin".let { value ->
value.length
}
println(result) // 6
このコードでは、
"Kotlin"がletの対象になる- その値がラムダ引数
valueに渡される value.lengthが評価される- その結果
6がletの返り値になる
という流れです。
ここで重要なのは、letは元のオブジェクトを返すとは限らないということです。
返すのはあくまでラムダの結果です。
itとは何か
letの説明でよく出てくるのがitです。
val result = "Kotlin".let {
it.length
}
これは先ほどの value を省略した書き方です。
Kotlinでは、ラムダ引数が1つだけの場合、その引数名を省略すると自動的にitになることがあります。
したがって、letの本質は「itを使うこと」ではありません。
正確には、対象オブジェクトをラムダ引数として受け取ることが本質です。
itは、その引数名を省略したときのデフォルト名にすぎません。
たとえば次の2つは同じ意味です。
val a = "abc".let {
it.length
}
val b = "abc".let { text ->
text.length
}
処理が短いときはitでも十分ですが、処理が長い場合やネストがある場合は、明示的に名前を付けた方が読みやすくなります。
letの最大の特徴は「ラムダ結果を返す」こと
letを理解するうえで最も重要なのは、返り値がラムダ結果であるという点です。
val x = "abc".let {
it.length
}
println(x) // 3
ここで返っているのは "abc" ではなく、it.length の結果である 3 です。
この性質があるため、letは次のような用途に向いています。
- 値を別の値に変換する
- チェーンの途中で整形する
- nullableな値をnon-nullとして安全に扱う
- 一時的なスコープを作る
nullableな値に対するlet
letが特によく使われるのは、safe call演算子 ?. と組み合わせる場合です。
val name: String? = "Kotlin"
name?.let {
println(it.length)
}
このコードでは、nameがnullでない場合にだけletのブロックが実行されます。
nameがnullなら、ブロック自体は実行されません。
この書き方は、次のような意図を簡潔に表せます。
この値がnullでないときだけ処理したい
たとえば次のようなコードです。
val email: String? = "test@example.com"
val lower = email?.let {
it.lowercase()
}
println(lower) // test@example.com
emailがnullでなければ小文字化され、nullなら結果もnullになります。
if (x != null)との関係
?.let { ... } は、実用上は if (x != null) と近い目的で使われます。
if (email != null) {
println(email.lowercase())
}
ただし、完全に同一の構文ではありません。
if (email != null) では、そのブロック内でsmart castが働きます。
一方 email?.let { ... } では、non-nullであることが確認された値がラムダ引数として閉じ込められる形になります。
そのため、厳密には内部の扱い方やスコープの見え方は異なります。
ただし学習上は、まず
?.let { ... }は「nullでないときだけ処理する」ための定番パターン
と理解して問題ありません。
letは変換処理と相性がよい
letは、ある値を受け取り、それを別の値に変換して返す処理と相性がよいです。
val result = "123".let {
it.toInt() * 2
}
println(result) // 246
この場合、
- 入力は
"123" - 処理は
toInt() * 2 - 出力は
246
です。
このように、letは入力値を受けて別の値を返すという形を自然に書けます。
ただし、「letは変換専用の関数である」という意味ではありません。
仕様としてそう決まっているわけではなく、返り値がラムダ結果であるため、変換用途に向いているという理解が正確です。
チェーンの途中で使うlet
letは、処理の途中結果を受けて整形したいときにも便利です。
val message = " hello kotlin "
.trim()
.uppercase()
.let { "結果: $it" }
println(message) // 結果: HELLO KOTLIN
このコードでは、
- 文字列を
trim()する uppercase()する- その結果を
letに渡して"結果: ..."の形に整形する
という流れです。
このように、処理チェーンの途中で一度値を受け取り、別の形に変換して返す場面でletはとても使いやすいです。
一時的なスコープを作る用途
letは、一時変数のスコープを限定したいときにも使えます。
val output = "kotlin".let { text ->
val capitalized = text.replaceFirstChar { it.uppercase() }
"Language: $capitalized"
}
println(output)
この例では capitalized は let の中でしか使えません。
つまり、その場限りの中間変数を外側に漏らさずに済むという利点があります。
処理を小さなまとまりとして閉じ込めたいときにも有効です。
letと他のスコープ関数の違い
Kotlinにはlet以外にもスコープ関数があります。
混乱しやすいので、まずは違いを整理しておくと理解しやすくなります。
let
- オブジェクトの受け取り方: ラムダ引数
- ブロック内での参照:
itまたは任意の引数名 - 返り値: ラムダ結果
val result = "abc".let {
it.length
}
also
- オブジェクトの受け取り方: ラムダ引数
- ブロック内での参照:
it - 返り値: 元のオブジェクト
val result = "abc".also {
println(it)
}
println(result) // abc
alsoは、途中でログ出力やデバッグ、通知などの副作用を挟みたいときに向いています。
処理後も元の値をそのまま流したい場合に使います。
run
- オブジェクトの受け取り方: ラムダレシーバ
- ブロック内での参照:
this(多くの場合省略される) - 返り値: ラムダ結果
val result = "abc".run {
length
}
runはletと同じくラムダ結果を返す点では似ています。
違うのは、letが「引数として受け取る」のに対し、runは「レシーバとして扱う」点です。
つまり、
let→it.lengthrun→length
のような書き方になります。
runは、対象オブジェクトのメンバーをまとめて扱いたいときに自然です。
apply
- オブジェクトの受け取り方: ラムダレシーバ
- ブロック内での参照:
this - 返り値: 元のオブジェクト
val list = mutableListOf<String>().apply {
add("A")
add("B")
}
applyは初期化や設定でよく使われます。
ただし「初期化専用」というわけではなく、オブジェクト自身を返しながら、レシーバとして操作したい場面全般で使えます。
letとrunの違いを正確に言うと
この2つは特に混同されやすいです。
共通点
- どちらもラムダ結果を返す
違い
letは対象オブジェクトをラムダ引数として扱うrunは対象オブジェクトをラムダレシーバとして扱う
例を並べると違いが見えやすいです。
val a = "Kotlin".let {
it.length
}
val b = "Kotlin".run {
length
}
どちらも結果は 6 ですが、内部の書き方が違います。
letが向いている場面
- 値を変換したい
- 引数として明示的に扱いたい
- 外側の
thisと区別したい
runが向いている場面
- オブジェクトのプロパティや関数をまとめて使いたい
thisを省略して書きたい
letとapplyの違いを正確に言うと
この2つは役割がかなり違います。
let
- ラムダ引数で受ける
- ラムダ結果を返す
val length = "abc".let {
it.length
}
apply
- ラムダレシーバで受ける
- 元のオブジェクトを返す
val list = mutableListOf<String>().apply {
add("A")
add("B")
}
なので、
- 別の値を返したいなら
let - オブジェクトを設定してそのまま返したいなら
apply
と考えると分かりやすいです。
実務でのよくある使い方
nullでないときだけ処理する
val userName: String? = "Shinichi"
userName?.let {
println(it.uppercase())
}
nullable値を変換する
val text: String? = "42"
val number = text?.let {
it.toInt()
}
println(number) // 42
チェーン途中で整形する
val result = " kotlin "
.trim()
.let { it.uppercase() }
.let { "Result: $it" }
println(result)
一時変数を閉じ込める
val message = "kotlin".let { text ->
val first = text.replaceFirstChar { it.uppercase() }
"Language: $first"
}
注意点: letの多用は読みにくくなることがある
letは便利ですが、使いすぎると逆に読みにくくなる場合があります。
特にネストしたときは注意が必要です。
user?.let {
profile?.let {
address?.let {
println(it.city)
}
}
}
このようなコードでは、どのitが何を指しているのか分かりにくくなります。
少なくとも、こうした方がまだ読みやすいです。
user?.let { userValue ->
profile?.let { profileValue ->
address?.let { addressValue ->
println(addressValue.city)
}
}
}
ただし、ここまでネストするなら、ifや早期returnの方が読みやすいことも多いです。
スコープ関数は便利ですが、可読性を優先して選ぶことが大切です。
初心者向けに一番大事な理解
letを難しく考えすぎないためには、まず次のように覚えるとよいです。
letは、値を一度ラムダ引数として受け取り、その処理結果を返す関数
そして実務では、まずこの2つを押さえるとかなり使いやすくなります。
?.let { ... }
→ nullでないときだけ処理するvalue.let { ... }
→ 値を受けて別の値に変換する
端的にまとめると
Kotlinのletは、対象オブジェクトをラムダ引数として受け取り、そのブロックの結果を返すスコープ関数です。
?.let { ... } の形ではnullでない場合だけ処理を実行でき、通常の let は変換・整形・一時スコープの作成に向いています。
alsoやapplyとの違いは、何を返すかとオブジェクトを引数として扱うか、レシーバとして扱うかにあります。
以上、Kotlinのletについてでした。
最後までお読みいただき、ありがとうございました。










