LoginSignup
0
0

More than 5 years have passed since last update.

ことりんと一緒 - 7. Scoping Function

Posted at

概要 / 説明

ことりんと一緒 Springもね - 6. 非同期処理」の中で、インスタンスのプロパティ設定を行う際に以下のような記述を行いました。

    @Bean
    fun normalTaskExecutor(): TaskExecutor  = ThreadPoolTaskExecutor().apply {
        corePoolSize = 1
        setQueueCapacity(5)
        maxPoolSize = 1
        setThreadNamePrefix("NormalThread-")
        setWaitForTasksToCompleteOnShutdown(true)
    }

これは、ThreadPoolTaskExecutor インスタンスを生成し、続けてラムダでそのインスタンスに対して操作を行っています。

このように、インスタンスに続けてラムダで操作を行う際に使用する関数を スコープ関数 と呼びます。
この書式を利用すると以下のような点で便利です。

  • インスタンスへの操作をラムダ内にまとめられる
  • コードの冗長性を省ける
  • 操作の影響範囲をラムダ内だけに収められる

Kotlin には、このスコープ関数が複数あります。
それぞれ使い方が異なりますので、どういうものか確認してみます。

前提 / 環境

以下の環境で、実施していますが特に前提ではありません。

ランタイムバージョン

  • Kotlin : 1.3.0
  • SpringBoot : 2.1.0.RELEASE

Spring Dependencies

  • Web
  • Actuator

開発環境

  • OS : Mac
  • IDE : IntelliJ IDEA
  • Build : Gradle

手順 / 解説

スコープ関数には以下のようなものがあります。

  • with
  • run
  • let
  • apply
  • also

それぞれの関数を見ていきます。

スコープ関数の定義

まず、それぞれのスコープ関数の定義を見てみます。

Scoping Function 定義
with fun with(receiver: T, block: T.() -> R): R = receiver.block() val r: R = with(T()) { this.foo(); this.toR() }
run fun T.run(block: T.() -> R): R = block() val r: R = T().run { this.foo(); this.toR() }
let fun T.let(block: (T) -> R): R = block(this) val r: R = T().let { it.foo(); it.toR() }
apply fun T.apply(block: T.() -> Unit): T { block(); return this } val t: T = T().apply { this.foo() }
also fun T.also(block: (T) -> Unit): T { block(this); return this } val t: T = T().also { it.foo() }

定義を見ると大きく以下の2つの観点での定義の仕方が異なっている事が分かります。

  • レシーバの表現
    • T.() -> R : レシーバ型Tの拡張関数
    • (T) -> R : レシーバ型Tを引数として扱う
  • 戻り値
    • R = block() : ラムダで実施した結果を返却
    • return this : レシーバを返却

上記の事をまとめると、次のようなマトリクスになります。

scopingfunction.png

コードサンプル

レシーバオブジェクトして次のデータクラスを用意しておきます。

data class Person(
        var name: String,
        var age: Int
) {
    private lateinit var hobby: String
    fun nameToUpperCase() {
        name = name.toUpperCase()
    }

    fun increaseAge() {
        age++
    }

    fun defineHobby(hobby: String) {
        this.hobby = hobby
    }

    fun displayHobby() = this.hobby

    fun toStringAddon(): String {
        return "Person(name=$name, age=$age, hobby=$hobby)"
    }
}

with

with は、レシーバの拡張関数ではなく、普通の関数です。
そのため、その他のスコープ関数とは呼び出し方が異なっています。

    val foo: Person = Person("with", 20)

    with(foo){
        foo.increaseAge()
        println(foo)
    }

run

run は、ラムダ内ではレシーバの表現を this で行います。
そのため、省略可能です。
以下では、明示的に記載していますが、冗長なので省略する事が多いです。
また、ラムダの結果(最終行)を返却します。

    val foo: Person = Person("run", 20)

    return foo.run {
        nameToUpperCase()
        println(this)
        this.name
    }

let

let は、run とほぼ同じ動きをしますが、
ラムダ内でのレシーバ表現が it です。
そのため、別名にする事が可能です。
意味のわかりやすいキーワードに置き換えて、
可読性をあげたりする時に使用します。

    val foo: Person = Person("let", 20)

    return foo.let { it ->
        println(it)
        it.defineHobby("Kotlin")
        println(it.toStringAddon())
        it.displayHobby()
    }

apply

apply は、レシーバ自身を返却します。
そのため、用途としてはレシーバ自身への操作を行う場合で
プロパティの設定や変更などが多い用途です。
レシーバの表現は this で行っています。
ここでは、レシーバの関数呼び出しで省略しています。

    val foo: Person = Person("apply", 20)

    foo.apply {
        println(this)
        nameToUpperCase()
        increaseAge()
        println(this)
    }

also

also は、apply のレシーバ表現が it となったものと考えてよいです。
そのため、用途としてはレシーバに別名を付けて可読性を上げる必要がある場合になります。

    val foo: Person = Person("also", 20)

    return foo.also { it ->
        println(it)
        it.nameToUpperCase()
        it.increaseAge()
        it.defineHobby("Kotlin")
        println(it.toStringAddon())
    }

まとめ / 振り返り

個人的には、スコープ関数の使い分けは戻り値でレシーバを返したいかどうかだけで判断しています。
あとは、this キーワードが煩雑になりそうな場合は控えるくらいで使い分けを行っています。

scopefunction-selection.png

今回のソース

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0