Kotlin: run, with, let, also и apply

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

Функции определения области видимости

Я сосредоточусь на функциях run , with , T.run , T.let , T.also и T.apply . Я называю их функциями области видимости, так как считаю, что их основная задача заключается в обеспечении внутренней области видимости для вызывающей функции. Самый простой способ проиллюстрировать область видимости — запустить функцию:

fun test() { 
    var mood = "Мне грустно"
    run  {
        val mood = "Я счастлив"
         println (mood) // Я счастлив
    }
     println (mood) // Мне грустно
}

Внутри функции test у вас может быть отдельная область, в которой mood переопределяется в "Я счастлив" перед печатью, и она полностью заключена в область run. Эта функция обзора кажется не очень полезной. Но есть одна приятная деталь, она возвращает последний объект в области видимости. Благодаря чему мы можем применить любую функцию (например show()) к обоим элементам, не записывая эту функцию “show()” дважды. Вот пример:

run  {
        if (firstTimeView) introView else normalView
     }.show()

Разница между функциями области видимости

Нормальная функция и функция расширения

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

with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}

webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

Однако их различие заключается в том, что одна из этих функций является – нормальной функцией “with“, а другая – функцией расширения “T.run” .

Итак, вопрос в том, в чем преимущество каждого из них? Представьте, что если webview.settings могли бы быть null, запись выглядела бы так:

with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
}

webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

В этом случае функция расширения T.run выглядит предпочтительнее, так как мы можем применить проверку на допустимость значения NULL в одном месте.

Аргументы this и it

Если мы посмотрим на T.run и T.let, обе функции будут похожи, за исключением того, что T.let принимает аргумент it. Ниже показана одинаковая логика для обеих функций:

stringVariable?.run {
      println("Длина этой строки равна $length")
}

stringVariable?.let {
      println("Длина этой строки равна ${it.length}")
}

Если вы присмотритесь то увидите, что T.run сделана как вызов функции расширения. Следовательно, все, что находится в пределах области действия, T может называться this. В Kotlin, this в большинстве случаев может быть опущено, поэтому в нашем примере выше мы cмогли написать просто $length вместо ${this.length}.

А вот T.let отправляет себя в функцию и это похоже на лямбда-аргумент. На него можно ссылаться в области видимости как it. Кажется, что T.run выглядит лучше, чем T.let, но есть некоторые преимущества T.let:

  • T.let обеспечивает более четкое различие между использованием переменной данной функции и переменных функций внешнего класса.
  • this нельзя опустить, например, когда он отправляется как параметр функции, и в этом случае it писать короче чем this и понятнее.
  • T.let позволяет лучше именовать преобразованную используемую переменную, т.е. вы можете преобразовать it в другое имя, как показано ниже:
stringVariable?.let {
      nonNullString ->
      println("Ненулевая строка $nonNullString")
}

Возвращать this или другие типы

Теперь давайте посмотрим на T.let и T.also, оба идентичны, если мы посмотрим на область его внутренней функции:

stringVariable?.let {
      println("Длина этой строки равна ${it.length}")
}

stringVariable?.also {
      println("Длина этой строки равна ${it.length}")
}

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

val original = "abc"

// let
original.let {
    println("Исходная строка $it") // "abc"
    it.reversed()
}.let {
    println("Обратная строка $it") // "cba"
    it.length
}.let {
    println("Длина строки $it") // 3
}

// also
original.also {
    println("Исходная строка $it") // "abc"
}.also {
    println("Обратная строка ${it.reversed()}") // "cba"
}.also {
    println("Длина строки ${it.length}") // 3
}

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

  1. T.also может обеспечить очень четкий процесс разделения на одних и тех же объектах, т.е. создание меньших функциональных секций.
  2. T.also может быть очень удобным для выполняя операции построения цепочки.

Пример использования T.let и T.also вместе:

// Нормальный подход
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// Улучшенный подход
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

Apply

Теперь рассмотрим функцию T.apply:

  1. Это функция расширения.
  2. Она отправляется this как аргумент.
  3. Она возвращает this (т.е. себя).

Вот пример применения apply:

// Нормальный подход
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
// Улучшенный подход, цепочка
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

Заключение

Для правильного выбора функции можно воспользоваться следующей шпаргалкой:

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

Поделись с друзьями:
Если вам понравилась статья, подписывайтесь на наши социальные сети.

Оставьте комментарий

десять + 2 =