概要 / 説明
「ことりんと一緒 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
: レシーバを返却
-
上記の事をまとめると、次のようなマトリクスになります。
コードサンプル
レシーバオブジェクトして次のデータクラスを用意しておきます。
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 キーワードが煩雑になりそうな場合は控えるくらいで使い分けを行っています。