layout | title |
---|---|
reference |
スコープ関数 |
Kotlin標準ライブラリには、オブジェクトのコンテキストでコードのブロックを実行する事だけを目的とするようないくつかの関数があります。
そのような関数をラムダ式を渡して呼び出せば、それは一時的なスコープを形成します。
このスコープの中ではそのオブジェクトを名前無しでアクセス出来ます。
そのような関数を**スコープ関数(scope functions)**といいます。
そのような関数が5つあります: let
, run
, with
, apply
, also
です。
基本的にはこれらの関数はすべて同じアクションを実行します: そのオブジェクトでコードのブロックを実行する。 違う所はこのオブジェクトがどのように使用出来るのかと式全体の結果が何なのか、という所だけです。
以下は、スコープ関数の典型的な使用例です:
{% capture let-ex %} 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 } {% endcapture %} {% include kotlin_quote.html body=let-ex %}
もし同じ例をlet
無しで書こうと思えば、新しい変数を導入して、その名前を使う都度何度も書かないといけません。
{% capture wo-let-ex %} 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 } {% endcapture %} {% include kotlin_quote.html body=wo-let-ex %}
スコープ関数は何か新しく技術的に出来る事が増える、というものではありません。ですが、コードをもっと簡潔で読みやすくしてくれます。
スコープ関数同士はとても似ている事から、適切なものを選ぶのはちょっと難しい事もあるかもしれません。 何を選ぶべきかは主に、あなたの意図と、プロジェクトの中の一貫性によって決まる類のものです。 以下では、スコープ関数の間の違いとそのコンベンションについて、詳細に説明します。
あなたが正しいスコープ関数を選びやすくするように、 ここにスコープ関数のキーとなる違いを要約したテーブルを示しておきます。
関数 | オブジェクトのリファレンス | 戻りの値 | 拡張関数か? |
---|---|---|---|
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つの点で異なります:
- コンテキストオブジェクトを参照する方法
- 戻りの値
スコープ関数にわたすラムダの中では、コンテキストオブジェクトはその実際の名前では無くて短いリファレンスで参照出来ます。
各スコープ関数はコンテキストオブジェクトを2つのうちのどちらかの方法で参照します: ラムダのレシーバ
(this
)か、ラムダの引数 (it
) かです。どちらも同じ機能を提供しますから、ここでは様々なユースケースの場合のそれらの長所と短所を説明し、
どういう時にどちらを使うべきかのオススメを伝授します。
{% capture diff-scope-for-same-ex %}
fun main() {
val str = "Hello"
// this
str.run {
println("文字列の長さ:
// it
str.let {
println("文字列の長さは ${it.length}")
}
} {% endcapture %} {% include kotlin_quote.html body=diff-scope-for-same-ex %}
run
, with
, apply
はコンテキストオブジェクトをラムダのレシーバとして参照します ー
つまり、キーワードthis
で参照します。
ようするに、ラムダの中ではオブジェクトは通常のクラスの関数の時のような感じで参照できます。
多くの場合、レシーバのオブジェクトのメンバにアクセスする時にはthis
を省略する事が出来て、コードが短く書けます。
一方、this
を省略するとレシーバのメンバなのか外側のオブジェクトのメンバや関数なのかを区別しづらくなります。
だから、コンテキストオブジェクトをレシーバとして持つ(this
)ものは、そのラムダが主にそのオブジェクトのメンバを呼び出したりプロパティに値を設定したり、といったようなオブジェクトに対する操作の場合に使うのが良いでしょう。
{% capture this-or-it %} 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 } {% endcapture %} {% include kotlin_quote.html body=this-or-it %}
一方、let
と also
はコンテキストオブジェクトをラムダの引数として参照します。
引数の名前を指定しなければ、オブジェクトは暗黙のデフォルトの名前、it
で参照出来ます。
it
はthis
よりも短いし、it
の式の方が通常は読みやすい事が多い。
しかしながら、オブジェクトの関数やプロパティを呼ぶ時には、this
のように暗黙にオブジェクトを参照する事は出来ません。
一方、コンテキストオブジェクトを主に関数呼び出しの引数などに渡したい時には、it
で参照する方が良いでしょう。
コードブロックで複数の変数を使いたい時にもit
の方がいいでしょう。
{% capture it-as-arg %} 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 } {% endcapture %} {% include kotlin_quote.html body=it-as-arg %}
以下の例ではコンテキストオブジェクトをラムダの名前をつけた引数 value
で参照する例です。
{% capture context-as-name %} 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 } {% endcapture %} {% include kotlin_quote.html body=context-as-name %}
スコープ関数は結果として返す値が異なっています:
apply
とalso
はコンテキストオブジェクトを返しますlet
,run
,with
はラムダの結果を返します
どの戻りの値が良いかは、あなたがコードの中で次に何をしたいかに基づいて良く考える必要があります。 この事が使うべき一番適切なスコープ関数を選ぶ事にもつながります。
apply
と also
の戻りの型はコンテキストオブジェクト自身です。
つまり、呼び出しチェーンの中に寄り道として含める事が出来ます:
同じオブジェクトに対して関数のチェーンを続ける事が出来ます。
{% capture ctx-obj-return %} fun main() { //sampleStart val numberList = mutableListOf() numberList.also { println("リストを作成します") } .apply { add(2.71) add(3.14) add(1.0) } .also { println("リストをソートします") } .sort() //sampleEnd println(numberList) } {% endcapture %} {% include kotlin_quote.html body=ctx-obj-return %}
コンテキストオブジェクトを返す関数のreturn文で使う事も出来ます。
{% capture return-also %} 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 } {% endcapture %} {% include kotlin_quote.html body=return-also %}
let
, run
, with
はラムダの結果を返します。
だから結果を変数に代入したり、結果にオペレーションをチェーンしたり、といった事が可能です。
{% capture lambda-result %} 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 } {% endcapture %} {% include kotlin_quote.html body=lambda-result %}
さらに、戻りの値を無視して、ローカル変数のための一時的なスコープを作るためにスコープ関数を使う事も出来ます。
{% capture temp-scope %}
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("最初の要素: $firstItem, 最後の要素: $lastItem")
}
//sampleEnd
}
{% endcapture %}
{% include kotlin_quote.html body=temp-scope %}
あなたのユースケースに合わせた適切なスコープ関数を選ぶ事を助けるために、 スコープ関数を詳細に説明して推奨する使い方を以下で説明します。 技術的にはスコープ関数は多くの場合に取り替え可能でどれを使う事も出来る場合が多々あるので、 以下の例では慣例的に何を使うかを示してもいます。
- コンテキストオブジェクトは引数(
it
)で扱える - 戻りの値はラムダの結果
let
は呼び出しチェーンの結果に対して関数を呼び出すのに使えます。
例えば以下の例では、コレクションの2つのオペレーションの結果をprintしていますが:
{% capture wo-let-callchain %}
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
//sampleEnd
}
{% endcapture %}
{% include kotlin_quote.html body=wo-let-callchain %}
let
を使えば、上のコードをリストオペレーションの結果を変数に代入しないように書き直す事が可能です:
{% capture with-let %} fun main() { //sampleStart val numbers = mutableListOf("one", "two", "three", "four", "five") numbers.map { it.length }.filter { it > 3 }.let { println(it) // もし必要ならここでさらに関数を呼び出す事もできる } //sampleEnd } {% endcapture %} {% include kotlin_quote.html body=with-let %}
もしlet
に渡しているコードブロックが引数がit
の関数一つの場合は、ラムダを引数にわたす代わりにメソッドリファレンス(::
)を渡す事も出来ます:
{% capture let-method-ref %} fun main() { //sampleStart val numbers = mutableListOf("one", "two", "three", "four", "five") numbers.map { it.length }.filter { it > 3 }.let(::println) //sampleEnd } {% endcapture %} {% include kotlin_quote.html body=let-method-ref %}
let
は非nullの値を含むコードブロックを実行するのに良く使われます。
非nullのオブジェクトにアクションを実行したい場合は、
そのオブジェクトにセーフコール演算子 ?.
を使用して、実行したいアクションをラムダで渡したlet
を呼び出します。
{% capture safe-call-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
}
{% endcapture %}
{% include kotlin_quote.html body=safe-call-let %}
限定した範囲内だけでローカル変数を導入する事でコードを読みやすくしたい、という時にもlet
を使う事が出来ます。
コンテキストオブジェクトを表す新しい変数を定義するためには、
ラムダの引数として名前を与える事で、デフォルトのit
の代わりとして使う事が出来ます。
{% capture let-for-newvar %} 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 } {% endcapture %} {% include kotlin_quote.html body=let-for-newvar %}
- コンテキストオブジェクトはレシーバ(
this
)として扱える - 戻りの値はラムダの結果
with
は拡張関数ではありません:
コンテキストオブジェクトは引数として渡されます。ですがラムダの中ではレシーバ(this
)として参照出来ます。
with
は、コンテキストオブジェクトに対して関数を呼び出して、その結果が必要無いような用途の時に使う事を推奨しています。
コードの中ではwith
は、以下の英文のように読む事が出来ます:"with this object, do the following."(このオブジェクトの対して、以下をしなさい)
{% capture with-ex1 %} fun main() { //sampleStart val numbers = mutableListOf("one", "two", "three") with(numbers) { println("'with'が引数 $this で呼び出されました") println("それは $size 要素を保持しています") } //sampleEnd } {% endcapture %} {% include kotlin_quote.html body=with-ex1 %}
with
をなんらかの値を計算するのに使うヘルパーオブジェクトを導入して、そのヘルパーオブジェクトのプロパティや関数を使って計算を行うような用途に使う事も出来ます。
{% capture with-ex2 %}
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"最初の要素は
- コンテキストオブジェクトはレシーバ(
this
)として扱える - 戻りの値はラムダの結果
run
は with
と同じ事を拡張関数で行います。
つまり、let
のように、コンテキストオブジェクトにドット記法で呼び出す事が出来ます。
run
はラムダでオブジェクトの初期化をしつつ結果の値を計算するような時に便利です。
{% capture run-ex1 %} 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) } {% endcapture %} {% include kotlin_quote.html body=run-ex1 %}
run
を拡張関数でなく実行する事も出来ます。
拡張関数でない版のrun
はコンテキストオブジェクトを持たず、
結果はラムダの結果を返します。
拡張関数でない版のrun
は、式が期待されている所に複数の文を書く事を可能にしてくれます。
{% capture run-ex2 %} 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 } {% endcapture %} {% include kotlin_quote.html body=run-ex2 %}
- コンテキストオブジェクトはレシーバ(
this
)として扱える - 戻りの値はオブジェクト自身
apply
はコンテキストオブジェクト自身を返すので、
コードブロックが値を返さずに、そのコードブロックの主な目的がレシーバオブジェクトのメンバを操作する事である場合に使う事をオススメしています。
apply
のもっとも良くある使われ方は、オブジェクトのコンフィギュレーションです。
そのような呼び出しは、以下のような英文のように読めます。
"apply the following assignments to the object." (オブジェクトに以下の代入を適用せよ)
{% capture apply-config %} 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
}
{% endcapture %}
{% include kotlin_quote.html body=apply-config %}
もうひとつよくある apply
の使用例としては、
複数の呼び出しチェーンの中により複雑な処理を含めたい場合が挙げられます。
- コンテキストオブジェクトは引数(
it
)で扱える - 戻りの値はオブジェクト自身
also
はコンテキストオブジェクトを引数に取るようなアクションを実行したい時に便利です。
also
はコンテキストオブジェクトのプロパティよりもコンテキストオブジェクト自身への参照を必要とするケースや、
外側のスコープのthis
をシャドー(隠す)してしまいたくない時に使いましょう。
also
をコードで見た時は、以下の英文のように読めます。"and also do the following with the object." (そしてさらに以下をオブジェクトにせよ)
{% capture also-ex %} fun main() { //sampleStart val numbers = mutableListOf("one", "two", "three") numbers .also { println("新しいのを足す前のリストの要素たち: $it") } .add("four") //sampleEnd } {% endcapture %} {% include kotlin_quote.html body=also-ex %}
スコープ関数に加えて、標準ライブラリにはtakeIf
と takeUnless
関数もあります。
これらの関数は呼び出しチェーンの中にオブジェクトの状態のチェックを含める事を可能にします。
takeIf
をオブジェクトに対して述語とともに呼び出すと、与えられた述語をオブジェクトが満たすならそのオブジェクトを返します。
そうでなければnull
を返します。つまり、takeIf
は単体のオブジェクトに対するフィルタ関数と言えます。
takeUnless
はtakeIf
の反対です。
takeUnless
をオブジェクトに対し述語とともに呼び出すと、オブジェクトが述語を満たすならnull
を返し、そうでなければそのオブジェクトを返します。
takeIf
やtakeUnlesss
の使用時には対象のオブジェクトはラムダの引数(it
)で触る事が出来ます。
{% capture takeif-ex1 %} 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 } {% endcapture %} {% include kotlin_quote.html body=takeif-ex1 %}
takeIf
やtakeUnless
のあとに他の関数をチェーンする場合は、つなげた方の関数でnullチェックをするかセーフコール(?.
)を使うのを忘れないようにしてください。 なぜなら返される値はnullableになるからです。
{: .tip}
{% capture takeif-ex2 %} fun main() { //sampleStart val str = "Hello" val caps = str.takeIf { it.isNotEmpty() }?.uppercase() //val caps = str.takeIf { it.isNotEmpty() }.uppercase() //コンパイルエラー println(caps) //sampleEnd } {% endcapture %} {% include kotlin_quote.html body=takeif-ex2 %}
takeIf
と takeUnless
はスコープ関数と併用するととりわけ便利です。
例えば、takeIf
や takeUnless
を let
と併用して、指定した述語にマッチしたオブジェクトに対してコードブロックを実行する事が出来ます。
これをt実現するためには、オブジェクトに対してtakeIf
を呼んで、そのあとにlet
をセーフコール(?
)で呼び出します。
述語にマッチしないオブジェクトに対してはtakeIf
はnull
を返すのでlet
は実行されないという訳です。
{% capture takeif-ex3 %} 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 } {% endcapture %} {% include kotlin_quote.html body=takeif-ex3 %}
比較のために、同じ事をtakeIf
やスコープ関数なしで書くと以下のようになります:
{% capture takeif-ex4 %} 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 } {% endcapture %} {% include kotlin_quote.html body=takeif-ex4 %}