Skip to content

スコープ関数

Kotlin標準ライブラリには、オブジェクトのコンテキスト内でコードブロックを実行することのみを目的とするいくつかの関数が含まれています。オブジェクトに対してラムダ式を渡してこれらの関数を呼び出すと、一時的なスコープが形成されます。このスコープ内では、オブジェクトの名前なしでアクセスできます。このような関数はスコープ関数と呼ばれます。これらには、letrunwithapplyalsoの5種類があります。

基本的に、これらの関数はすべて同じアクションを実行します。つまり、オブジェクトに対してコードブロックを実行します。異なるのは、このオブジェクトがブロック内でどのように利用可能になるか、および式全体の戻り値が何かという点です。

スコープ関数の典型的な使用例を以下に示します。

kotlin
data class Person(var name: String, var age: Int, var city: String) {
    fun moveTo(newCity: String) { city = newCity }
    fun incrementAge() { age++ }
}

fun main() {
    Person("Alice", 20, "Amsterdam").let {
        println(it)
        it.moveTo("London")
        it.incrementAge()
        println(it)
    }
}

letなしで同じコードを記述する場合、新しい変数を導入し、それを使用するたびに名前を繰り返す必要があります。

kotlin
data class Person(var name: String, var age: Int, var city: String) {
    fun moveTo(newCity: String) { city = newCity }
    fun incrementAge() { age++ }
}

fun main() {
    val alice = Person("Alice", 20, "Amsterdam")
    println(alice)
    alice.moveTo("London")
    alice.incrementAge()
    println(alice)
}

スコープ関数は、新しい技術的な機能を追加するものではありませんが、コードをより簡潔で読みやすくすることができます。

スコープ関数には多くの類似点があるため、ユースケースに合った適切なものを選択するのは難しい場合があります。選択は主に、意図とプロジェクトでの一貫した使用方法に依存します。以下に、スコープ関数の違いとその慣例について詳細な説明を記述します。

関数選択

目的のために適切なスコープ関数を選択するのに役立つよう、それらの主な違いをまとめた以下の表を示します。

関数オブジェクト参照戻り値拡張関数であるか
letitラムダの結果はい
runthisラムダの結果はい
run-ラムダの結果いいえ: コンテキストオブジェクトなしで呼び出される
withthisラムダの結果いいえ: コンテキストオブジェクトを引数として取る
applythisコンテキストオブジェクトはい
alsoitコンテキストオブジェクトはい

これらの関数に関する詳細は、以下の専用セクションで提供されています。

以下に、目的に応じたスコープ関数の選び方に関する簡単なガイドを示します。

  • null許容型ではないオブジェクトでラムダを実行する: let
  • ローカルスコープで式を変数として導入する: let
  • オブジェクトの構成: apply
  • オブジェクトの構成と結果の計算: run
  • 式が必要な場所でステートメントを実行する: 非拡張関数版のrun
  • 追加の効果: also
  • オブジェクトに対する関数呼び出しのグループ化: with

異なるスコープ関数のユースケースは重複しているため、プロジェクトやチームで使用されている特定の慣例に基づいて、どの関数を使用するかを選択できます。

スコープ関数はコードをより簡潔にすることができますが、使いすぎは避けてください。コードが読みにくくなり、エラーにつながる可能性があります。また、スコープ関数のネストを避け、チェーンする際には注意することをお勧めします。現在のコンテキストオブジェクトとthisまたはitの値について混乱しやすいからです。

区別

スコープ関数は本質的に似ているため、それらの違いを理解することが重要です。 各スコープ関数には主に2つの違いがあります。

  • コンテキストオブジェクトを参照する方法。
  • それらの戻り値。

コンテキストオブジェクト: thisまたはit

スコープ関数に渡されるラムダ内では、コンテキストオブジェクトは実際の名前の代わりに短い参照で利用できます。各スコープ関数は、コンテキストオブジェクトを参照するために2つの方法のいずれかを使用します。ラムダのレシーバー (this) として、またはラムダの引数 (it) としてです。どちらも同じ機能を提供するため、異なるユースケースにおけるそれぞれの長所と短所を説明し、その使用に関する推奨事項を提供します。

kotlin
fun main() {
    val str = "Hello"
    // this
    str.run {
        println("The string's length: $length")
        //println("The string's length: ${this.length}") // does the same
    }

    // it
    str.let {
        println("The string's length is ${it.length}")
    }
}

this

runwith、およびapplyは、コンテキストオブジェクトをラムダのレシーバーとして、キーワードthisで参照します。したがって、それらのラムダ内では、オブジェクトは通常のクラス関数と同じように利用できます。

ほとんどの場合、レシーバーオブジェクトのメンバーにアクセスするときにthisを省略できるため、コードが短くなります。一方、thisが省略されている場合、レシーバーのメンバーと外部のオブジェクトや関数を区別するのが難しくなることがあります。したがって、コンテキストオブジェクトをレシーバー (this) として持つことは、主にオブジェクトの関数を呼び出したり、プロパティに値を割り当てたりすることで、オブジェクトのメンバーを操作するラムダに推奨されます。

kotlin
data class Person(var name: String, var age: Int = 0, var city: String = "")

fun main() {
    val adam = Person("Adam").apply { 
        age = 20                       // same as this.age = 20
        city = "London"
    }
    println(adam)
}

it

次に、letalsoは、コンテキストオブジェクトをラムダの引数として参照します。引数名が指定されていない場合、オブジェクトは暗黙のデフォルト名itでアクセスされます。itthisよりも短く、itを使用する式は通常読みやすいです。

ただし、オブジェクトの関数やプロパティを呼び出す場合、thisのようにオブジェクトを暗黙的に利用することはできません。したがって、オブジェクトが関数呼び出しの引数として主に使用される場合、itを介してコンテキストオブジェクトにアクセスする方が優れています。また、コードブロック内で複数の変数を使用する場合にもitは優れています。

kotlin
import kotlin.random.Random

fun writeToLog(message: String) {
    println("INFO: $message")
}

fun main() {
    fun getRandomInt(): Int {
        return Random.nextInt(100).also {
            writeToLog("getRandomInt() generated value $it")
        }
    }
    
    val i = getRandomInt()
    println(i)
}

以下の例は、引数名valueを使用してコンテキストオブジェクトをラムダ引数として参照する方法を示しています。

kotlin
import kotlin.random.Random

fun writeToLog(message: String) {
    println("INFO: $message")
}

fun main() {
    fun getRandomInt(): Int {
        return Random.nextInt(100).also { value ->
            writeToLog("getRandomInt() generated value $value")
        }
    }
    
    val i = getRandomInt()
    println(i)
}

戻り値

スコープ関数は、返す結果によって異なります。

  • applyalsoはコンテキストオブジェクトを返します。
  • letrun、およびwithはラムダの結果を返します。

コードで次に何をしたいかに基づいて、どのような戻り値が必要かを慎重に検討する必要があります。これにより、使用する最適なスコープ関数を選択できます。

コンテキストオブジェクト

applyalsoの戻り値は、コンテキストオブジェクト自体です。したがって、これらは_サイドステップ_として呼び出しチェーンに含めることができます。つまり、同じオブジェクトに対して関数呼び出しを次々にチェーンし続けることができます。

kotlin
fun main() {
    val numberList = mutableListOf<Double>()
    numberList.also { println("Populating the list") }
        .apply {
            add(2.71)
            add(3.14)
            add(1.0)
        }
        .also { println("Sorting the list") }
        .sort()
    println(numberList)
}

また、コンテキストオブジェクトを返す関数のreturn文で使用することもできます。

kotlin
import kotlin.random.Random

fun writeToLog(message: String) {
    println("INFO: $message")
}

fun main() {
    fun getRandomInt(): Int {
        return Random.nextInt(100).also {
            writeToLog("getRandomInt() generated value $it")
        }
    }
    
    val i = getRandomInt()
}

ラムダの結果

letrun、およびwithはラムダの結果を返します。そのため、結果を変数に割り当てたり、結果に対する操作をチェーンしたりする場合などに使用できます。

kotlin
fun main() {
    val numbers = mutableListOf("one", "two", "three")
    val countEndsWithE = numbers.run { 
        add("four")
        add("five")
        count { it.endsWith("e") }
    }
    println("There are $countEndsWithE elements that end with e.")
}

さらに、戻り値を無視して、スコープ関数を使用してローカル変数の一時的なスコープを作成することもできます。

kotlin
fun main() {
    val numbers = mutableListOf("one", "two", "three")
    with(numbers) {
        val firstItem = first()
        val lastItem = last()        
        println("First item: $firstItem, last item: $lastItem")
    }
}

関数

ユースケースに合った適切なスコープ関数を選択できるように、各関数を詳細に説明し、使用に関する推奨事項を提供します。技術的には、スコープ関数は多くの場合で相互に交換可能であるため、例ではそれらの使用に関する慣例を示します。

let

  • コンテキストオブジェクトは引数 (it) として利用できます。
  • 戻り値はラムダの結果です。

letは、呼び出しチェーンの結果に対して1つまたは複数の関数を呼び出すために使用できます。たとえば、以下のコードはコレクションに対する2つの操作の結果を出力します。

kotlin
fun main() {
    val numbers = mutableListOf("one", "two", "three", "four", "five")
    val resultList = numbers.map { it.length }.filter { it > 3 }
    println(resultList)    
}

letを使用すると、リスト操作の結果を変数に割り当てないように、上記の例を書き換えることができます。

kotlin
fun main() {
    val numbers = mutableListOf("one", "two", "three", "four", "five")
    numbers.map { it.length }.filter { it > 3 }.let { 
        println(it)
        // and more function calls if needed
    } 
}

letに渡されるコードブロックにitを引数とする単一の関数が含まれている場合、ラムダ引数の代わりにメソッド参照 (::) を使用できます。

kotlin
fun main() {
    val numbers = mutableListOf("one", "two", "three", "four", "five")
    numbers.map { it.length }.filter { it > 3 }.let(::println)
}

letは、nullではない値を含むコードブロックを実行するためによく使用されます。nullではないオブジェクトに対してアクションを実行するには、そのオブジェクトに対して安全呼び出し演算子?.を使用し、そのラムダ内のアクションでletを呼び出します。

kotlin
fun processNonNullString(str: String) {}

fun main() {
    val str: String? = "Hello"   
    //processNonNullString(str)       // compilation error: str can be null
    val length = str?.let { 
        println("let() called on $it")        
        processNonNullString(it)      // OK: 'it' is not null inside '?.let { }'
        it.length
    }
}

letを使用すると、限られたスコープを持つローカル変数を導入して、コードを読みやすくすることもできます。コンテキストオブジェクトの新しい変数を定義するには、その名前をラムダ引数として指定し、デフォルトのitの代わりに使用できるようにします。

kotlin
fun main() {
    val numbers = listOf("one", "two", "three", "four")
    val modifiedFirstItem = numbers.first().let { firstItem ->
        println("The first item of the list is '$firstItem'")
        if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
    }.uppercase()
    println("First item after modifications: '$modifiedFirstItem'")
}

with

  • コンテキストオブジェクトはレシーバー (this) として利用できます。
  • 戻り値はラムダの結果です。

withは拡張関数ではないため、コンテキストオブジェクトは引数として渡されますが、ラムダ内ではレシーバー (this) として利用できます。

戻り値を使用する必要がない場合に、コンテキストオブジェクト上で関数を呼び出す際にはwithを使用することをお勧めします。コードでは、withは「このオブジェクトに対して、以下を実行する」と読むことができます。

kotlin
fun main() {
    val numbers = mutableListOf("one", "two", "three")
    with(numbers) {
        println("'with' is called with argument $this")
        println("It contains $size elements")
    }
}

withを使用すると、プロパティや関数が値の計算に使用されるヘルパーオブジェクトを導入することもできます。

kotlin
fun main() {
    val numbers = mutableListOf("one", "two", "three")
    val firstAndLast = with(numbers) {
        "The first element is ${first()}," +
        " the last element is ${last()}"
    }
    println(firstAndLast)
}

run

  • コンテキストオブジェクトはレシーバー (this) として利用できます。
  • 戻り値はラムダの結果です。

runwithと同じことをしますが、拡張関数として実装されています。したがって、letと同様に、ドット表記を使用してコンテキストオブジェクト上で呼び出すことができます。

runは、ラムダがオブジェクトを初期化し、戻り値を計算する両方を行う場合に役立ちます。

kotlin
class MultiportService(var url: String, var port: Int) {
    fun prepareRequest(): String = "Default request"
    fun query(request: String): String = "Result for query '$request'"
}

fun main() {
    val service = MultiportService("https://example.kotlinlang.org", 80)

    val result = service.run {
        port = 8080
        query(prepareRequest() + " to port $port")
    }
    
    // the same code written with let() function:
    val letResult = service.let {
        it.port = 8080
        it.query(it.prepareRequest() + " to port ${it.port}")
    }
    println(result)
    println(letResult)
}

runを非拡張関数として呼び出すこともできます。非拡張関数版のrunにはコンテキストオブジェクトがありませんが、それでもラムダの結果を返します。非拡張関数版のrunを使用すると、式が必要な場所で複数のステートメントのブロックを実行できます。コードでは、非拡張関数版のrunは「コードブロックを実行し、結果を計算する」と読むことができます。

kotlin
fun main() {
    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)
    }
}

apply

  • コンテキストオブジェクトはレシーバー (this) として利用できます。
  • 戻り値はオブジェクト自体です。

applyはコンテキストオブジェクト自体を返すため、値を返さず、主にレシーバーオブジェクトのメンバーを操作するコードブロックに使用することをお勧めします。applyの最も一般的なユースケースは、オブジェクトの構成です。このような呼び出しは、「オブジェクトに以下の代入を適用する」と読むことができます。

kotlin
data class Person(var name: String, var age: Int = 0, var city: String = "")

fun main() {
    val adam = Person("Adam").apply {
        age = 32
        city = "London"        
    }
    println(adam)
}

applyのもう一つのユースケースは、より複雑な処理のために複数の呼び出しチェーンにapplyを含めることです。

also

  • コンテキストオブジェクトは引数 (it) として利用できます。
  • 戻り値はオブジェクト自体です。

alsoは、コンテキストオブジェクトを引数として取る何らかのアクションを実行するのに役立ちます。オブジェクトのプロパティや関数ではなく、オブジェクトへの参照が必要なアクション、または外側のスコープからのthis参照をシャドウしたくない場合にalsoを使用します。

コードでalsoを見た場合、それは「さらに、そのオブジェクトに対して以下を実行する」と読むことができます。

kotlin
fun main() {
    val numbers = mutableListOf("one", "two", "three")
    numbers
        .also { println("The list elements before adding new one: $it") }
        .add("four")
}

takeIftakeUnless

スコープ関数に加えて、標準ライブラリにはtakeIftakeUnless関数が含まれています。これらの関数を使用すると、オブジェクトの状態のチェックを呼び出しチェーンに組み込むことができます。

オブジェクトに対して述語とともに呼び出された場合、takeIfはそのオブジェクトが与えられた述語を満たせば、そのオブジェクトを返します。そうでなければ、nullを返します。したがって、takeIfは単一のオブジェクトに対するフィルタリング関数です。

takeUnlesstakeIfとは逆のロジックを持っています。オブジェクトに対して述語とともに呼び出された場合、takeUnlessはそのオブジェクトが与えられた述語を満たせば、nullを返します。そうでなければ、オブジェクトを返します。

takeIfまたはtakeUnlessを使用する場合、オブジェクトはラムダ引数 (it) として利用できます。

kotlin
import kotlin.random.*

fun main() {
    val number = Random.nextInt(100)

    val evenOrNull = number.takeIf { it % 2 == 0 }
    val oddOrNull = number.takeUnless { it % 2 == 0 }
    println("even: $evenOrNull, odd: $oddOrNull")
}

TIP

takeIftakeUnlessの後に他の関数をチェーンする場合、戻り値がnull許容型であるため、nullチェックを実行するか安全呼び出し (?.) を使用することを忘れないでください。

kotlin
fun main() {
    val str = "Hello"
    val caps = str.takeIf { it.isNotEmpty() }?.uppercase()
   //val caps = str.takeIf { it.isNotEmpty() }.uppercase() //compilation error
    println(caps)
}

takeIftakeUnlessは、スコープ関数と組み合わせると特に便利です。たとえば、takeIftakeUnlessletとチェーンすることで、与えられた述語に一致するオブジェクトに対してコードブロックを実行できます。これを行うには、オブジェクトに対してtakeIfを呼び出し、次に安全呼び出し (?) を使用してletを呼び出します。述語に一致しないオブジェクトの場合、takeIfnullを返し、letは呼び出されません。

kotlin
fun main() {
    fun displaySubstringPosition(input: String, sub: String) {
        input.indexOf(sub).takeIf { it >= 0 }?.let {
            println("The substring $sub is found in $input.")
            println("Its start position is $it.")
        }
    }

    displaySubstringPosition("010000011", "11")
    displaySubstringPosition("010000011", "12")
}

比較のために、takeIfやスコープ関数を使用せずに同じ関数を記述する方法の例を以下に示します。

kotlin
fun main() {
    fun displaySubstringPosition(input: String, sub: String) {
        val index = input.indexOf(sub)
        if (index >= 0) {
            println("The substring $sub is found in $input.")
            println("Its start position is $index.")
        }
    }

    displaySubstringPosition("010000011", "11")
    displaySubstringPosition("010000011", "12")
}