Kotlinのletについて

採用はこちら

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

このコードでは、

  1. "Kotlin"let の対象になる
  2. その値がラムダ引数 value に渡される
  3. value.length が評価される
  4. その結果 6let の返り値になる

という流れです。

ここで重要なのは、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)
}

このコードでは、namenullでない場合にだけletのブロックが実行されます。

namenullなら、ブロック自体は実行されません。

この書き方は、次のような意図を簡潔に表せます。

この値がnullでないときだけ処理したい

たとえば次のようなコードです。

val email: String? = "test@example.com"

val lower = email?.let {
    it.lowercase()
}

println(lower) // test@example.com

emailnullでなければ小文字化され、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

このコードでは、

  1. 文字列をtrim()する
  2. uppercase()する
  3. その結果をletに渡して "結果: ..." の形に整形する

という流れです。

このように、処理チェーンの途中で一度値を受け取り、別の形に変換して返す場面でletはとても使いやすいです。

一時的なスコープを作る用途

letは、一時変数のスコープを限定したいときにも使えます。

val output = "kotlin".let { text ->
    val capitalized = text.replaceFirstChar { it.uppercase() }
    "Language: $capitalized"
}

println(output)

この例では capitalizedlet の中でしか使えません。

つまり、その場限りの中間変数を外側に漏らさずに済むという利点があります。

処理を小さなまとまりとして閉じ込めたいときにも有効です。

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
}

runletと同じくラムダ結果を返す点では似ています。

違うのは、letが「引数として受け取る」のに対し、runは「レシーバとして扱う」点です。

つまり、

  • letit.length
  • runlength

のような書き方になります。

runは、対象オブジェクトのメンバーをまとめて扱いたいときに自然です。

apply

  • オブジェクトの受け取り方: ラムダレシーバ
  • ブロック内での参照: this
  • 返り値: 元のオブジェクト
val list = mutableListOf<String>().apply {
    add("A")
    add("B")
}

applyは初期化や設定でよく使われます。

ただし「初期化専用」というわけではなく、オブジェクト自身を返しながら、レシーバとして操作したい場面全般で使えます。

letrunの違いを正確に言うと

この2つは特に混同されやすいです。

共通点

  • どちらもラムダ結果を返す

違い

  • letは対象オブジェクトをラムダ引数として扱う
  • runは対象オブジェクトをラムダレシーバとして扱う

例を並べると違いが見えやすいです。

val a = "Kotlin".let {
    it.length
}

val b = "Kotlin".run {
    length
}

どちらも結果は 6 ですが、内部の書き方が違います。

letが向いている場面

  • 値を変換したい
  • 引数として明示的に扱いたい
  • 外側のthisと区別したい

runが向いている場面

  • オブジェクトのプロパティや関数をまとめて使いたい
  • thisを省略して書きたい

letapplyの違いを正確に言うと

この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つを押さえるとかなり使いやすくなります。

  1. ?.let { ... }
    → nullでないときだけ処理する
  2. value.let { ... }
    → 値を受けて別の値に変換する

端的にまとめると

Kotlinのletは、対象オブジェクトをラムダ引数として受け取り、そのブロックの結果を返すスコープ関数です。

?.let { ... } の形ではnullでない場合だけ処理を実行でき、通常の let は変換・整形・一時スコープの作成に向いています。

alsoapplyとの違いは、何を返すかオブジェクトを引数として扱うか、レシーバとして扱うかにあります。

以上、Kotlinのletについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次