Kotlin:スコープ関数(apply,let,with,run,also)

投稿日:  更新日:

スコープ関数はKotlinの標準ライブラリで提供されています。

そのことから、Kotlinを特徴付ける重要な構文であることが分かります。

ただし、「プログラムの動作を定義する構文」ではなく、「プログラムのコードの品質を上げる構文」です。

同様な動作はスコープ関数を使わなくても記述できます。しかし、スコープ関数を使えばスマートな(洗練された)記述になります。

積極的に使いたいと思いますが、applyとlet以外はあまり使う機会がありません。また、稀にwithなどが登場すると、コードの流れが理解できずに戸惑います。

ですので、スコープ関数について、まとめました。

※環境:Android Studio Hedgehog | 2023.1.1 Patch 2
    Kotlin 1.9.0
    Compose Compiler 1.5.1

スポンサーリンク

スコープ関数とは

スコープ関数は、オブジェクト(レシーバー)が参照可能な範囲(スコープ)で、コードブロック(ラムダ式)を実行する関数です。

スコープ関数の概要

「apply、run、also、with、let」の5つがあります。各々の特徴は表の通りです。

スコープ関数レシーバー(T)戻り値(R)
T.applythisレシーバー
T.run(タイプ2)ラムダ式の結果
T.alsoitレシーバー
with(T)ラムダ式の結果
T.let
run(タイプ1)なし
※ラムダ式の戻り値は{ }内で最後に評価した式の値

使用頻度が高いのは「apply」と「let」です。

※「Kotlinの標準ライブラリ Scope functions」も合わせて参照して下さい。

スポンサーリンク

apply

オブジェクトへ一連の設定を行った後に構築を行うようなBuilderパターン(デザインパターンの一種)を採用したクラスで利用されます。

fun Greeting_Apply(name: String): String {
    val _builder = StringBuilder(name).apply {
        insert(0, "[[ Hello ")		// 設定1
        append(" !! ]]")			// 設定2
    }
    return _builder.toString()		// 構築
}

apply関数は拡張関数です。レシーバーはコードブロック内でthisにより参照できます。

戻り値はレシーバーです。

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

alsoとの違いはレシーバーの参照方法のみです。また、run(タイプ2)との違いは戻り値のみです。

スポンサーリンク

also

用途はapplyと同じです。

fun Greeting_Also(name: String): String {
    val _builder = StringBuilder(name).also {
        it.insert(0, "[[ Hello ")	// 設定1
        it.append(" !! ]]")			// 設定2
    }
    return _builder.toString()		// 構築
}

also関数は拡張関数です。しかし、レシーバーはラムダ式の引数で与えられるため、コードブロック内でitにより参照できます。

戻り値はレシーバーです。

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

applyとの違いはレシーバーの参照方法のみです。

スポンサーリンク

with

オブジェクトとそれを使った処理の関係を強調したい場合に利用されます。関係を強調すると、一括りにグループ化した印象を持たせることが出来ます。

fun Greeting_With(name: String): String {
    return with(StringBuilder(name)) {
        insert(0, "[[ Hello ")
        append(" !! ]]")
        toString()
    }
}

with関数は一般的な関数です。レシーバーは関数の引数で与えられますが、コードブロックがレシーバ付きラムダ式なので、コードブロック内でthisにより参照できます。

戻り値はラムダ式の結果です。

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

let

オブジェクトのnon-nullable判定の分岐で利用されます。

letの例let未使用の例
fun Greeting_Let(name: String?): String {
    return name?.let {							// name ≠ nullの時
        val _builder = StringBuilder(name)
            .insert(0, "[[ Hello ")
            .append(" !! ]]")
        _builder.toString()
    }?: "[[ Hello Mr.X !! ]]"					// name = nullの時
}
fun Greeting_noLet(name: String?): String {
    return if(name != null) {
        val _builder = StringBuilder(name)
            .insert(0, "[[ Hello ")
            .append(" !! ]]")
        _builder.toString()
    } else "[[ Hello Mr.X !! ]]"
}

let関数は拡張関数です。しかし、レシーバーはラムダ式の引数で与えられるため、コードブロック内でitにより参照できます。

戻り値はラムダ式の結果です。

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
スポンサーリンク

run(タイプ1)

処理をローカル化したい時に利用されます。ローカル化された処理は各々が独立しています。ですので、変数はその内部でのみ有効です。

fun Greeting_Run1(name: String): Pair<String, String> {
    val _value1 = run {						// ローカルブロック1
        val _builder = StringBuilder(name)  // 同じ名前(_builder)が利用可能
            .insert(0, "[[ Hello ")
            .append(" !! ]]")
        _builder.toString()
    }
    val _value2 = run {						// ローカルブロック2
        val _builder = StringBuilder(name)  // 同じ名前(_builder)が利用可能
            .insert(0, "[[ Hi !! ")
            .append(" !! ]]")
        _builder.toString()
    }
    return Pair(_value1, _value2)
}

run(タイプ1)は一般的な関数です。レシーバ―は持ちません。コードブロックの実行のみを行います。

戻り値はラムダ式の結果です。

@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
スポンサーリンク

run(タイプ2)

用途はapplyと同じです。

fun Greeting_Run2(name: String): String {
    return StringBuilder(name).run {
        insert(0, "[[ Hello ")
        append(" !! ]]")
        toString()
    }
}

run(タイプ2)関数は拡張関数です。レシーバーはコードブロック内でthisにより参照できます。

戻り値はラムダ式の結果です。

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

applyとの違いは戻り値のみです。

スポンサーリンク

Appendix(サンプルの実行結果)

        System.out.println(Greeting_Apply("Taro"))
        System.out.println(Greeting_Run1("Jiro").first)
        System.out.println(Greeting_Run1("Jiro").second)
        System.out.println(Greeting_Run2("Tama"))
        System.out.println(Greeting_Also("Yuri"))
        System.out.println(Greeting_With("Mako"))
        System.out.println(Greeting_Let("Poci"))
        System.out.println(Greeting_Let(null))
        System.out.println(Greeting_noLet("Hati"))
        System.out.println(Greeting_noLet(null))
[[ Hello Taro !! ]]
[[ Hello Jiro !! ]]
[[ Hi !! Jiro !! ]]
[[ Hello Tama !! ]]
[[ Hello Yuri !! ]]
[[ Hello Mako !! ]]
[[ Hello Poci !! ]]
[[ Hello Mr.X !! ]]
[[ Hello Hati !! ]]
[[ Hello Mr.X !! ]]
スポンサーリンク

関連記事:

KDocのドキュメント記述例を紹介します。 KDocを使えばKotlinのソースコードに書いたコメントからドキュメントを作成することが出来ます。後々、クラスの再利用を考えているなら、ドキュメントを残して再利用性を高めておきましょう! ...
Kotlinは関数を変数に代入したり、引数で受け渡したりできます。 関数を第一級オブジェクトで扱えるためです。 これにより、関数を使った処理の委譲が容易になりました。ArrayOf#forEach( )がその典型的な例です。 以上のような操作で必要となる「関数オブジェクト」について、まとめます。 ...
クロージャ(Closure)は新しい概念ではなく、関数型プログラミングに古くから存在していました。 手続き型やオブジェクト指向プログラミングにも、だいぶ前から採用が進んでいます。 Kotlinもクロージャが使える言語の1つです。 ...
Androidのアプリを作成しているとSAM変換が頻繁に登場します。 プログラムが簡素に記述できることから、プログラマーに好評なようです。 Kotlinが関数を第一級オブジェクトとして扱える恩恵です。 しかし、私はSAM変換に出合うと、いつもプログラムの論理が把握できなくて戸惑います。なので、苦手です。あまり好きではありません。 苦手を克服するために、もっとSAM変換の理解を深めたいと思い、まとめてみました。 ...
近頃の携帯端末はクワッドコア(プロセッサが4つ)やオクタコア(プロセッサが8つ)が当たり前になりました。 サクサク動作するアプリを作るために、この恩恵を使わなければ損です。 となると、必然的に非同期処理(マルチスレッド)を使うことになります。 JavaのThreadクラス、Android APIのAsyncTaskクラスが代表的な手法です。 Kotlinは上記に加えて「コルーチン(Coroutine)」が使えるようになっています。 今回は、このコルーチンについて、まとめます。 ...
Android Studio Giraffe(2023.07)になって、ビルドスクリプトの推奨がKotlin DSLになりました。 この機会に、ビルドスクリプトと記述言語についてまとめます。 ※環境:Android Studio Giraffe | 2022.3.1    :Android Gradle Plugin 8.1.0    :Gradle 8.0 ...
Kotlinにおいて、各クラスのインスタンスに自動生成されるequals()関数は、classとdata classで比較対象に違いがあります。 紛らさしいので、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10 ...
KotlinはJavaのプリミティブ型に当たるデータ型を持ちません。しかし、同等な記述が基本データ型を用いて出来ます。 Kotlinにおいて、基本データ型がどのように扱われているのか、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10 ...
継承を行うことなく新しい機能(関数やプロパティ)をクラスへ追加する「拡張機能」についてまとめます。 何処にでも手軽に定義できるため、とても重宝する仕組みですが、乱用するとプログラムが無法地帯になるので、管理は重要です。そのために、十分な理解が必要になります。 ちなみに、Android Jetpackは、この「拡張機能」を活用して作られた部分が多くあります。「拡張機能」無くして、成り立たないシステムです。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 1     Kotlin 1.8.10 ...
スポンサーリンク