Spec-Zone.ru › Kotlin 1.6

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

Стандартная библиотека 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

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

Да

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

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

  • Выполнение лямбды на не-null объектах: 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 or adam.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 часто используется для выполнения блока кода только с ненулевыми значениями. Чтобы выполнить действия над ненулевым объектом, используйте оператор безопасного вызова ?. и вызовите 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 или безопасный вызов (?.), потому что их возвращаемое значение может быть null.

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
}
Последнее изменение: 07 апреля 2022 г.
Операции над картами Требования к включению

© 2010–2022 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