Spec-Zone.ru › Kotlin 1.7

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

Стандартная библиотека 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 часто используется для выполнения блока кода только с ненулевыми значениями. Чтобы выполнить действия над ненулевым объектом, используйте оператор безопасного вызова ?. над ним и вызовите 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
}
Последнее изменение: 06 сентября 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