Как стать автором
Обновить

Функции области видимости (Scope Function) в Kotlin

Время на прочтение7 мин
Количество просмотров15K

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

В статье используется термин "функции области видимости" для "Scope Function". Это определение взято из перевода документации на русский язык.

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

Что такое функции области видимости

В Kotlin есть 5 функций: let, run, with, apply и also, объединенных общим названием Scope Function (функции области видимости). Все они используются для одной цели - выполнить какой-то блок кода для конкретного объекта. Почему их так назвали? Потому что они меняют способ взаимодействия и видимость для этой переменной.

В основном они отличаются только 2 параметрами: способом ссылки на объект и возвращаемым параметром.

Давайте сначала приведем пример использования:

let

val length = "test".let{
    println(it)
  	it.length
}
  • Объект "test" внутри блока доступен как it

  • Возвращает результат выполнения lambda-функции

also

val test = "test".also{     
  println(it)   
}
  • Объект "test" внутри блока доступен как it

  • Возвращает контекстный объект ("test")

apply

val moscow = City("Moscow").apply{
  this.population = 15_000_000
  println(this)
}
  • Объект City("Moscow") внутри блока доступен как this (поэтому для поля popultaion - мы можем опустить обращения и будет population=15_000_000)

  • Возвращает контекстный объект (изменённый City("Moscow"))

run (с контекстным объектом)

val optimalSquare = City("Moscow").run {
    this.population = 15_000_000
  	this.solveOptimalSquare()
}
  • Объект City("Moscow") внутри блока доступен как this (поэтому для поля popultaion - мы можем опустить обращения и будет population=15_000_000)

  • Возвращает результат выполнения lambda-функции (solveOptimalSquare())

run (без контекстного объекта)

val length = run {
  val test = "test"
  test.length
}
  • Нет объекта на котором применятся

  • Возвращает результат выполнения lambda-функции (test.length)

with

val length = with("test"){
  this.length
}
  • Объект "test" внутри блока доступен как this

  • Возвращает результат выполнения lambda-функции (this.length)

Как видно, функции очень похожи друг на друга. Для того чтобы разобраться как они работают нужно разобраться в понятии extension function (здесь и далее будет использован перевод "функции расширения")

Функции расширения

Функции расширения в Kotlin позволяют расширять классы, не наследуясь от них. С помощью них мы можем добавить к существующим классам свои методы. Функции расширения таким образом заменяют утилитные классы (например, StringUtils от Apache).

Давайте рассмотрим упрощенный пример из стандартной библиотеки Kotlin

public fun CharSequence?.isNullOrBlank(): Boolean {
    return this == null || this.isBlank()
}

Как видно в качестве класса для расширения также можно использовать null-допустимые классы.

Как это работает:

  • Мы указываем тип получателя (reciever type)

  • Ссылаемся на объект этого типа как this

Давайте посмотрим, во что компилируется функция расширения.

Исходный код:

fun main() {
    println("test".firstSymbol())
}

public fun String.firstSymbol(): Char{
    return this[0]
}

Bytecode:

  public static final char firstSymbol(java.lang.String);
    descriptor: (Ljava/lang/String;)C
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #27                 // String <this>
         3: invokestatic  #33                 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
         6: aload_0
         7: iconst_0
         8: invokevirtual #39                 // Method java/lang/String.charAt:(I)C
        11: ireturn
      LineNumberTable:
        line 6: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0 $this$firstSymbol   Ljava/lang/String;
    RuntimeInvisibleParameterAnnotations:
      parameter 0:
        0: #25()
          org.jetbrains.annotations.NotNull

Соответствующий java-code:

public static final char firstSymbol(@NotNull String $this$firstSymbol) {
  Intrinsics.checkNotNullParameter($this$firstSymbol, "$this$firstSymbol");
  return $this$firstSymbol.charAt(0);
}

Как видно, она комплируется в статический метод, где первым параметром выступает объект, на котором применяется функция расширения.

Как раз поэтому функция расширения не может получить доступ к приватным полям и методам и поэтому функции расширения вычиляются статически.

//Код НЕ рабочий
fun main() {
    println(Test("test").firstSymbol())
}

class Test(private val value: String)

public fun Test.firstSymbol(): Char{
// поле является приватным и из статического метода к нему нет доступа
    return this.value[0] //ОШИБКА
}

Следующий пример взят из документации

open class Shape
class Rectangle: Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
    println(s.getName())
}

printClassName(Rectangle())

Распечатается Shape

Как работают функции области видимости

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

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

let

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

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

also

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

also - очень похож на let, но возвращается объект this (объект, на котором вызывается also)

apply

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

Функция apply устроена довольно интересно. Она является функцией расширения, при этом как параметр она принимет lambda-функцию, которая тоже является расширением для того же типа. Поэтому вызов block() здесь нужно рассматривать как вызов this.block(). Возвращается объект, на котором была вызвана функция apply

run (с контекстным объектом)

public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

run очень похож на apply, но возвращает результат выполнения this.block()

run (без контекстного объекта)

public inline fun <R> run(block: () -> R): R {
    return block()
}

Не является функцией расширения, и принимет обычную lambda-функцию. Возвращает результат выполнения этой lambda-функции.

with

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

Не является функцией расширением. Принимает два параметра - объект и функция расширения, которая будет вызываться на нем. Возвращает результат этого выполнения.

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

Исходный код:

fun main() {
    val length = "test".let {
        println(it)
        it.length
    }
    println(length)
}

Bytecode:

  public static final void main();
    descriptor: ()V
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=7, args_size=0
         0: ldc           #8                  // String test
         2: astore_1
         3: iconst_0
         4: istore_2
         5: iconst_0
         6: istore_3
         7: aload_1
         8: astore        4
        10: iconst_0
        11: istore        5
        13: iconst_0
        14: istore        6
        16: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream;
        19: aload         4
        21: invokevirtual #20                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        24: aload         4
        26: invokevirtual #26                 // Method java/lang/String.length:()I
        29: nop
        30: istore_0
        31: iconst_0
        32: istore_1
        33: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream;
        36: iload_0
        37: invokevirtual #29                 // Method java/io/PrintStream.println:(I)V
        40: return

Близко-соответствующий java-code (часть служебных переменных удалено):

   public static final void main() {
      String var1 = "test";
      System.out.println(var1);
      int length = var1.length();
      System.out.println(length);
   }

Как видно, здесь нет никакого упоминания об let

Что когда применять

Какую функцию когда применять - вопрос довольно сложный и дискуссионный.

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

Самая главная рекомендация - не переусложняйте код, он должен быть легко читаем и однозначен. Чем сложнее код - тем больше ошибок мы можем в нем совершить. И помним, что IDEA у нас не всегда под рукой, например, часто простые исправления проверяются online, например, в gitlab, где нет таких возможностей как в IDEA.

Основные грамматические отличия можно свести в таблицу:

Функция будет принимать this

Функция будет принимать it

Будет возвращен объект на котором вызывается функция (self)

apply

also

Будет возвращен результат функции (result)

run, with

let

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

Большую часть функций удобно использовать для реализации сокращенной записи (см. ниже пример с apply)

let

  • часто используется для безопасного выполнения блока кода с null-выражениями

val b: Int? = null

val a = b?.let { nonNullable -> nonNullable } ?: "Equal to 'null' or not set"
println(a)

also

  • используется для выполнения каких-либо дополнительных действий

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

apply

  • настроить объекта и не надо возвращать результат (удобно использовать для настройки Spring beans (бинов)

val registrar = DateTimeFormatterRegistrar().apply {
  setUseIsoFormat(true)
  registerFormatters(registry)
}

run, with

  • run и with очень похожи, поэтому не рекомендуется использовать их вместе.

  • run - используется для настройки объекта и вычисления результата

fun printAlphabet() = StringBuilder().run{
    for (letter in 'A'..'Z'){
        append(letter)
    }
    toString()
}
  • run без контекстного объекта - выполнение набора операций в отдельной зоне видимости

  • with - используется для объединения вызовов функций объекта

// выводим все буквы алфавита
fun printAlphabet() = with(StringBuilder()){
    for (letter in 'A'..'Z'){
        append(letter)
    }
    toString()
}

Использованные материалы

Теги:
Хабы:
Всего голосов 7: ↑5 и ↓2+4
Комментарии4

Публикации

Истории

Работа

Java разработчик
347 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань