Edit Page
スコープ関数
Kotlin標準ライブラリには、オブジェクトのコンテキストでコードのブロックを実行する事だけを目的とするようないくつかの関数があります。
そのような関数をラムダ式を渡して呼び出せば、それは一時的なスコープを形成します。
このスコープの中ではそのオブジェクトを名前無しでアクセス出来ます。
そのような関数をスコープ関数(scope functions)といいます。
そのような関数が5つあります: let, run
, with, apply
, alsoです。
基本的にはこれらの関数はすべて同じアクションを実行します: そのオブジェクトでコードのブロックを実行する。
違う所はこのオブジェクトがどのように使用出来るのかと式全体の結果が何なのか、という所だけです。
以下は、スコープ関数の典型的な使用例です:
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
//sampleStart
Person("アリス", 20, "アムステルダム").let {
println(it)
it.moveTo("ロンドン")
it.incrementAge()
println(it)
}
//sampleEnd
}
もし同じ例をlet無しで書こうと思えば、新しい変数を導入して、その名前を使う都度何度も書かないといけません。
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
//sampleStart
val alice = Person("アリス", 20, "アムステルダム")
println(alice)
alice.moveTo("ロンドン")
alice.incrementAge()
println(alice)
//sampleEnd
}
スコープ関数は何か新しく技術的に出来る事が増える、というものではありません。ですが、コードをもっと簡潔で読みやすくしてくれます。
スコープ関数同士はとても似ている事から、適切なものを選ぶのはちょっと難しい事もあるかもしれません。
何を選ぶべきかは主に、あなたの意図と、プロジェクトの中の一貫性によって決まる類のものです。
以下では、スコープ関数の間の違いとそのコンベンションについて、詳細に説明します。
関数の選択
あなたが正しいスコープ関数を選びやすくするように、
ここにスコープ関数のキーとなる違いを要約したテーブルを示しておきます。
| 関数 |
オブジェクトのリファレンス |
戻りの値 |
拡張関数か? |
let |
it |
ラムダの結果 |
Yes |
run |
this |
ラムダの結果 |
Yes |
run |
- |
ラムダの結果 |
No: コンテキストオブジェクト無しで呼ぶ |
with |
this |
ラムダの結果 |
No: コンテキストオブジェクトを引数に取る |
apply |
this |
コンテキストオブジェクト |
Yes |
also |
it |
コンテキストオブジェクト |
Yes |
これらの関数の詳細な情報については、以下のそれぞれのセクションで提供します。
以下は意図している目的に応じてスコープ関数を選ぶための短いガイドです:
- 非nullableなオブジェクトにラムダを実行する:
let
- 式の結果を変数としてローカルスコープに導入したい:
let
- オブジェクトのコンフィギュレーション:
apply
- オブジェクトのコンフィギュレーションと結果の計算:
run
- 式が要求される所で文を実行したい:拡張で無い方の
run
- 追加の効果:
also
- オブジェクトに対する関数をグルーピングしたい:
with
異なるスコープ関数のユースケースの一部はかぶっています。
だからそのスコープ関数を使うかはプロジェクトやチームでどのようなコンベンションになっているかによって選んでよろしい。
スコープ関数はあなたのコードをより簡潔にしてくれるものではありますが、使い過ぎには注意しましょう:
使いすぎるとコードが読みにくくなり、それはエラーへとつながる事もあります。
また、我々のおすすめとしては、スコープ関数をネストするのはやめて、スコープ関数をチェインするのも身長になった方が良いでしょう。
それらをすると、すぐにそれぞれのブロックのコンテキストのオブジェクトや、そこでのthisやitの値がなんなのか混乱しがちだからです。
(スコープ関数の)違い
スコープ関数は本質的にお互いに似ているものなので、
それらの間の「違い」を理解するのが大切です。
各スコープ関数は主に2つの点で異なります:
コンテキストオブジェクト: this か it
スコープ関数にわたすラムダの中では、コンテキストオブジェクトはその実際の名前では無くて短いリファレンスで参照出来ます。
各スコープ関数はコンテキストオブジェクトを2つのうちのどちらかの方法で参照します: ラムダのレシーバ
(this)か、ラムダの引数 (it) かです。どちらも同じ機能を提供しますから、ここでは様々なユースケースの場合のそれらの長所と短所を説明し、
どういう時にどちらを使うべきかのオススメを伝授します。
fun main() {
val str = "Hello"
// this
str.run {
println("文字列の長さ: $length")
//println("文字列の長さ: ${this.length}") // 同じ意味
}
// it
str.let {
println("文字列の長さは ${it.length}")
}
}
this
run, with, applyはコンテキストオブジェクトをラムダのレシーバとして参照します ー
つまり、キーワードthisで参照します。
ようするに、ラムダの中ではオブジェクトは通常のクラスの関数の時のような感じで参照できます。
多くの場合、レシーバのオブジェクトのメンバにアクセスする時にはthisを省略する事が出来て、コードが短く書けます。
一方、thisを省略するとレシーバのメンバなのか外側のオブジェクトのメンバや関数なのかを区別しづらくなります。
だから、コンテキストオブジェクトをレシーバとして持つ(this)ものは、そのラムダが主にそのオブジェクトのメンバを呼び出したりプロパティに値を設定したり、といったようなオブジェクトに対する操作の場合に使うのが良いでしょう。
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
//sampleStart
val adam = Person("アダム").apply {
age = 20 // this.age = 20 と同じ
city = "ロンドン"
}
println(adam)
//sampleEnd
}
it
一方、let と also はコンテキストオブジェクトをラムダの引数として参照します。
引数の名前を指定しなければ、オブジェクトは暗黙のデフォルトの名前、itで参照出来ます。
itはthisよりも短いし、itの式の方が通常は読みやすい事が多い。
しかしながら、オブジェクトの関数やプロパティを呼ぶ時には、thisのように暗黙にオブジェクトを参照する事は出来ません。
一方、コンテキストオブジェクトを主に関数呼び出しの引数などに渡したい時には、itで参照する方が良いでしょう。
コードブロックで複数の変数を使いたい時にもitの方がいいでしょう。
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
//sampleStart
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() は値 $it を生成する")
}
}
val i = getRandomInt()
println(i)
//sampleEnd
}
以下の例ではコンテキストオブジェクトをラムダの名前をつけた引数 value で参照する例です。
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
//sampleStart
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() は値 $value を生成する")
}
}
val i = getRandomInt()
println(i)
//sampleEnd
}
戻りの値
スコープ関数は結果として返す値が異なっています:
apply と also はコンテキストオブジェクトを返します
let, run, with はラムダの結果を返します
どの戻りの値が良いかは、あなたがコードの中で次に何をしたいかに基づいて良く考える必要があります。
この事が使うべき一番適切なスコープ関数を選ぶ事にもつながります。
コンテキストオブジェクト
apply と also の戻りの型はコンテキストオブジェクト自身です。
つまり、呼び出しチェーンの中に寄り道として含める事が出来ます:
同じオブジェクトに対して関数のチェーンを続ける事が出来ます。
fun main() {
//sampleStart
val numberList = mutableListOf<Double>()
numberList.also { println("リストを作成します") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("リストをソートします") }
.sort()
//sampleEnd
println(numberList)
}
コンテキストオブジェクトを返す関数のreturn文で使う事も出来ます。
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
//sampleStart
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
//sampleEnd
}
ラムダの結果
let, run, withはラムダの結果を返します。
だから結果を変数に代入したり、結果にオペレーションをチェーンしたり、といった事が可能です。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("末尾がeで終わる要素は $countEndsWithE 個あります")
//sampleEnd
}
さらに、戻りの値を無視して、ローカル変数のための一時的なスコープを作るためにスコープ関数を使う事も出来ます。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("最初の要素: $firstItem, 最後の要素: $lastItem")
}
//sampleEnd
}
実際の関数たち
あなたのユースケースに合わせた適切なスコープ関数を選ぶ事を助けるために、
スコープ関数を詳細に説明して推奨する使い方を以下で説明します。
技術的にはスコープ関数は多くの場合に取り替え可能でどれを使う事も出来る場合が多々あるので、
以下の例では慣例的に何を使うかを示してもいます。
let
- コンテキストオブジェクトは引数(
it)で扱える
- 戻りの値はラムダの結果
letは呼び出しチェーンの結果に対して関数を呼び出すのに使えます。
例えば以下の例では、コレクションの2つのオペレーションの結果をprintしていますが:
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
//sampleEnd
}
letを使えば、上のコードをリストオペレーションの結果を変数に代入しないように書き直す事が可能です:
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// もし必要ならここでさらに関数を呼び出す事もできる
}
//sampleEnd
}
もしletに渡しているコードブロックが引数がitの関数一つの場合は、ラムダを引数にわたす代わりにメソッドリファレンス(::)を渡す事も出来ます:
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
//sampleEnd
}
letは非nullの値を含むコードブロックを実行するのに良く使われます。
非nullのオブジェクトにアクションを実行したい場合は、
そのオブジェクトにセーフコール演算子 ?.を使用して、実行したいアクションをラムダで渡したletを呼び出します。
fun processNonNullString(str: String) {}
fun main() {
//sampleStart
val str: String? = "Hello"
//processNonNullString(str) // コンパイルエラー: strはnullかもしれないから
val length = str?.let {
println("$it に対し、let()を呼び出した")
processNonNullString(it) // OK: '?.let { }'の中の'it'はnullでは無いから
it.length
}
//sampleEnd
}
限定した範囲内だけでローカル変数を導入する事でコードを読みやすくしたい、という時にもletを使う事が出来ます。
コンテキストオブジェクトを表す新しい変数を定義するためには、
ラムダの引数として名前を与える事で、デフォルトのitの代わりとして使う事が出来ます。
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("リストの最初の要素は '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("修正した後の最初の要素: '$modifiedFirstItem'")
//sampleEnd
}
with
- コンテキストオブジェクトはレシーバ(
this)として扱える
- 戻りの値はラムダの結果
withは拡張関数ではありません:
コンテキストオブジェクトは引数として渡されます。ですがラムダの中ではレシーバ(this)として参照出来ます。
withは、コンテキストオブジェクトに対して関数を呼び出して、その結果が必要無いような用途の時に使う事を推奨しています。
コードの中ではwithは、以下の英文のように読む事が出来ます:”with this object, do the following.“(このオブジェクトの対して、以下をしなさい)
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with'が引数 $this で呼び出されました")
println("それは $size 要素を保持しています")
}
//sampleEnd
}
withをなんらかの値を計算するのに使うヘルパーオブジェクトを導入して、そのヘルパーオブジェクトのプロパティや関数を使って計算を行うような用途に使う事も出来ます。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"最初の要素は ${first()}、 " +
"最後の要素は ${last()}"
}
println(firstAndLast)
//sampleEnd
}
run
- コンテキストオブジェクトはレシーバ(
this)として扱える
- 戻りの値はラムダの結果
run は with と同じ事を拡張関数で行います。
つまり、letのように、コンテキストオブジェクトにドット記法で呼び出す事が出来ます。
runはラムダでオブジェクトの初期化をしつつ結果の値を計算するような時に便利です。
class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "デフォルトのリクエスト"
fun query(request: String): String = "クエリ '$request' の結果"
}
fun main() {
//sampleStart
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " をポート $port に")
}
// let() 関数を使って同じコードを書いてみる:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " をポート ${it.port} に")
}
//sampleEnd
println(result)
println(letResult)
}
runを拡張関数でなく実行する事も出来ます。
拡張関数でない版のrunはコンテキストオブジェクトを持たず、
結果はラムダの結果を返します。
拡張関数でない版のrunは、式が期待されている所に複数の文を書く事を可能にしてくれます。
fun main() {
//sampleStart
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
println(match.value)
}
//sampleEnd
}
apply
- コンテキストオブジェクトはレシーバ(
this)として扱える
- 戻りの値はオブジェクト自身
applyはコンテキストオブジェクト自身を返すので、
コードブロックが値を返さずに、そのコードブロックの主な目的がレシーバオブジェクトのメンバを操作する事である場合に使う事をオススメしています。
applyのもっとも良くある使われ方は、オブジェクトのコンフィギュレーションです。
そのような呼び出しは、以下のような英文のように読めます。
“apply the following assignments to the object.” (オブジェクトに以下の代入を適用せよ)
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
//sampleStart
val adam = Person("アダム").apply {
age = 32
city = "ロンドン"
}
println(adam)
//sampleEnd
}
もうひとつよくある apply の使用例としては、
複数の呼び出しチェーンの中により複雑な処理を含めたい場合が挙げられます。
also
- コンテキストオブジェクトは引数(
it)で扱える
- 戻りの値はオブジェクト自身
alsoはコンテキストオブジェクトを引数に取るようなアクションを実行したい時に便利です。
alsoはコンテキストオブジェクトのプロパティよりもコンテキストオブジェクト自身への参照を必要とするケースや、
外側のスコープのthisをシャドー(隠す)してしまいたくない時に使いましょう。
alsoをコードで見た時は、以下の英文のように読めます。”and also do the following with the object.” (そしてさらに以下をオブジェクトにせよ)
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("新しいのを足す前のリストの要素たち: $it") }
.add("four")
//sampleEnd
}
takeIf と takeUnless
スコープ関数に加えて、標準ライブラリにはtakeIf と takeUnless
関数もあります。
これらの関数は呼び出しチェーンの中にオブジェクトの状態のチェックを含める事を可能にします。
takeIfをオブジェクトに対して述語とともに呼び出すと、与えられた述語をオブジェクトが満たすならそのオブジェクトを返します。
そうでなければnullを返します。つまり、takeIfは単体のオブジェクトに対するフィルタ関数と言えます。
takeUnlessはtakeIfの反対です。
takeUnlessをオブジェクトに対し述語とともに呼び出すと、オブジェクトが述語を満たすならnullを返し、そうでなければそのオブジェクトを返します。
takeIfやtakeUnlesssの使用時には対象のオブジェクトはラムダの引数(it)で触る事が出来ます。
import kotlin.random.*
fun main() {
//sampleStart
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("偶数: $evenOrNull, 奇数: $oddOrNull")
//sampleEnd
}
takeIfやtakeUnlessのあとに他の関数をチェーンする場合は、つなげた方の関数でnullチェックをするかセーフコール(?.)を使うのを忘れないようにしてください。
なぜなら返される値はnullableになるからです。
fun main() {
//sampleStart
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.uppercase()
//val caps = str.takeIf { it.isNotEmpty() }.uppercase() //コンパイルエラー
println(caps)
//sampleEnd
}
takeIf と takeUnless はスコープ関数と併用するととりわけ便利です。
例えば、takeIf や takeUnless を let と併用して、指定した述語にマッチしたオブジェクトに対してコードブロックを実行する事が出来ます。
これをt実現するためには、オブジェクトに対してtakeIfを呼んで、そのあとにletをセーフコール(?)で呼び出します。
述語にマッチしないオブジェクトに対してはtakeIfはnullを返すのでletは実行されないという訳です。
fun main() {
//sampleStart
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("部分文字列 $sub は $input の中に見つかりました。")
println("その開始位置は $it です。")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
//sampleEnd
}
比較のために、同じ事をtakeIfやスコープ関数なしで書くと以下のようになります:
fun main() {
//sampleStart
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("部分文字列 $sub は $input の中に見つかりました。")
println("その開始位置は $index です。")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
//sampleEnd
}