Spec-Zone.ru › Kotlin 1.4

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

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

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

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

Отличия

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

  • Способ ссылки на контекстный объект
  • Значение возврата.

Контекстный объект: this или it

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

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

    // it
    str.let {
        println("The receiver 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()
//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()
//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)
}

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

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 + "!"
    }.toUpperCase()
    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("+1234 -FFFF not-a-number")) {
        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
}
END_OF_DOCUMENT_MARKER

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

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

Функция Ссылка на объект Значение возврата Функция расширения
let it Результат лямбда-выражения Да
run this Результат лямбда-выражения Да
run - Результат лямбда-выражения Нет: вызывается без объекта контекста
with this Результат лямбда-выражения Нет: принимает объект контекста в качестве аргумента.
apply this Объект контекста Да
also it Объект контекста Да

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

  • Выполнение лямбда-выражения на непустых объектах: let
  • Введение выражения как переменной в локальной области видимости: let
  • Настройка объекта: apply
  • Настройка объекта и вычисление результата: run
  • Выполнение операторов, где требуется выражение: не функция расширения run
  • Дополнительные эффекты: also
  • Группировка вызовов функций на объекте: with

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

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

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() }?.toUpperCase()
   //val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() //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
}

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

Spec-Zone.ru

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