Spec-Zone.ru › Kotlin 1.8

Функции области видимости

Стандартная библиотека Kotlin содержит несколько функций, единственной целью которых является выполнение блока кода в контексте объекта. Когда вы вызываете такую функцию на объекте с предоставленным лямбда-выражением, она формирует временную область видимости. В этой области видимости вы можете получить доступ к объекту без его имени. Такие функции называются функциями области видимости. Их пять: 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("Alice", 20, "Amsterdam").let {
        println(it)
        it.moveTo("London")
        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("Alice", 20, "Amsterdam")
    println(alice)
    alice.moveTo("London")
    alice.incrementAge()
    println(alice)
//sampleEnd
}

Функции области видимости не вводят никаких новых технических возможностей, но они могут сделать ваш код более лаконичным и читабельным.

Из-за схожего характера функций области видимости выбор подходящей для вашего случая может быть немного сложным. Выбор в основном зависит от ваших намерений и согласованности использования в вашем проекте. Ниже мы предоставим подробные описания различий между функциями области видимости и соглашениями по их использованию.

Выбор функции

Чтобы помочь вам выбрать подходящую функцию области видимости для вашей цели, мы предоставляем таблицу ключевых различий между ними.

Функция

Ссылка на объект

Возвращаемое значение

Является ли расширяющей функцией

let

it

Результат лямбды

Да

run

this

Результат лямбды

Да

run

-

Результат лямбды

Нет: вызывается без объекта контекста

with

this

Результат лямбды

Нет: принимает объект контекста в качестве аргумента.

apply

this

Объект контекста

Да

also

it

Объект контекста

Да

Подробная информация о различиях приведена в отдельных разделах ниже.

Вот краткое руководство по выбору функций области видимости в зависимости от предполагаемой цели:

  • Выполнение лямбды на непустых объектах: let

  • Введение выражения как переменной в локальной области: let

  • Настройка объекта: apply

  • Настройка объекта и вычисление результата: run

  • Выполнение операторов, где требуется выражение: не-расширяющая run

  • Дополнительные эффекты: also

  • Группировка вызовов функций на объекте: with

Сферы применения разных функций перекрываются, поэтому вы можете выбирать функции в зависимости от конкретных соглашений, используемых в вашем проекте или команде.

Хотя функции области видимости — способ сделать код более лаконичным, избегайте их чрезмерного использования: это может снизить читаемость кода и привести к ошибкам. Избегайте вложенных функций области видимости и будьте осторожны при их объединении: легко запутаться в текущем объекте контекста и значении this или it.

Отличия

Поскольку все функции области видимости имеют довольно схожий характер, важно понять различия между ними. Существует два основных отличия между каждой функцией области видимости:

  • Способ обращения к объекту контекста.

  • Возвращаемое значение.

Объект контекста: this или it

Внутри лямбды функции области видимости объект контекста доступен с помощью короткой ссылки вместо его фактического имени. Каждая функция области видимости использует один из двух способов доступа к объекту контекста: в качестве получателя лямбды (this) или в качестве аргумента лямбды (it). Оба обеспечивают одинаковые возможности, поэтому мы опишем преимущества и недостатки каждого для различных случаев и предоставим рекомендации по их использованию.

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

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("Adam").apply { 
        age = 20                       // same as this.age = 20
        city = "London"
    }
    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() generated value $it")
        }
    }
    
    val i = getRandomInt()
    println(i)
//sampleEnd
}

Кроме того, когда вы передаете объект контекста как аргумент, вы можете указать пользовательское имя для объекта контекста внутри области видимости.

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() generated value $value")
        }
    }
    
    val i = getRandomInt()
    println(i)
//sampleEnd
}

Возвращаемое значение

Функции области видимости различаются по возвращаемому значению:

  • apply и also возвращают объект контекста.

  • let, run, и with возвращают результат лямбды.

Эти два варианта позволяют выбрать соответствующую функцию в зависимости от того, что вы сделаете дальше в вашем коде.

Объект контекста

Возвращаемое значение apply и also — это сам объект контекста. Таким образом, они могут быть включены в цепочки вызовов как побочные шаги: вы можете продолжить цепочку вызовов функций на том же объекте после них.

fun main() {
//sampleStart
    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()
//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("There are $countEndsWithE elements that end with e.")
//sampleEnd
}

Кроме того, вы можете игнорировать возвращаемое значение и использовать функцию области видимости для создания временной области видимости для переменных.

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

Функции

Чтобы помочь вам выбрать подходящую функцию области действия для вашего случая, мы подробно опишем их и предоставим рекомендации по использованию. Технически, функции взаимозаменяемы во многих случаях, поэтому примеры показывают соглашения, которые определяют общий стиль использования.

let

Объект контекста доступен в качестве аргумента (it). Значение возврата — результат лямбды.

let может использоваться для вызова одной или нескольких функций по результатам цепочки вызовов. Например, следующий код выводит результаты двух операций над коллекцией:

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)
        // and more function calls if needed
    } 
//sampleEnd
}

Если блок кода содержит единственную функцию с 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)       // 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
    }
//sampleEnd
}

Еще один случай использования let — введение локальных переменных с ограниченной областью видимости для повышения читабельности кода. Чтобы определить новую переменную для объекта контекста, укажите её имя в качестве аргумента лямбды, чтобы можно было использовать её вместо значения по умолчанию it.

fun main() {
//sampleStart
    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'")
//sampleEnd
}

with

Функция без расширения: объект контекста передаётся в качестве аргумента, но внутри лямбды он доступен как получатель (this). Значение возврата — результат лямбды.

Мы рекомендуем with для вызова функций на объекте контекста без предоставления результата лямбды. В коде with можно прочитать как "с этим объектом выполните следующее."

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

Еще один случай использования with — введение вспомогательного объекта, свойства или функции которого будут использоваться для вычисления значения.

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

run

Объект контекста доступен как получатель (this). Значение возврата — результат лямбды.

run делает то же, что и with, но вызывает как let — как функцию расширения объекта контекста.

run полезна, когда ваша лямбда содержит как инициализацию объекта, так и вычисление возвращаемого значения.

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

fun main() {
//sampleStart
    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}")
    }
//sampleEnd
    println(result)
    println(letResult)
}

Помимо вызова 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 — конфигурация объекта. Такие вызовы можно читать как "применить следующие присваивания к объекту."

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

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

Имея объект-получатель в качестве возвращаемого значения, вы можете легко включить apply в цепочки вызовов для более сложной обработки.

also

Объект контекста доступен в качестве аргумента (it). Значение возврата — сам объект.

also хорошо подходит для выполнения некоторых действий, принимающих объект контекста в качестве аргумента. Используйте also для действий, которым нужна ссылка на объект, а не его свойства и функции, или когда вы не хотите затенять ссылку this из внешней области видимости.

Когда вы видите also в коде, вы можете прочитать его как "и также выполните следующее с объектом."

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

takeIf и takeUnless

Помимо функций области действия, в стандартной библиотеке есть функции takeIf и takeUnless. Эти функции позволяют встраивать проверки состояния объекта в цепочки вызовов.

При вызове над объектом с предоставленным предикатом, takeIf возвращает этот объект, если он соответствует предикату. В противном случае, он возвращает null. Таким образом, takeIf — это функция фильтрации для одного объекта. В свою очередь, takeUnless возвращает объект, если он не соответствует предикату, и null, если соответствует. Объект доступен в качестве аргумента лямбды (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("even: $evenOrNull, odd: $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() //compilation error
    println(caps)
//sampleEnd
}

takeIf и takeUnless особенно полезны в сочетании с функциями области действия. Хороший случай — объединение их с let для выполнения блока кода на объектах, соответствующих заданному предикату. Для этого вызовите takeIf на объекте, а затем вызовите let с безопасным вызовом (?). Для объектов, не соответствующих предикату, takeIf возвращает null, и let не вызывается.

fun main() {
//sampleStart
    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")
//sampleEnd
}

Вот как та же функция выглядит без функций стандартной библиотеки:

fun main() {
//sampleStart
    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")
//sampleEnd
}
Последнее изменение: 10 января 2023
Операции, специфичные для карт Требования к включению

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/scope-functions.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API