пятница, 2 августа 2019 г.

Осознание стандартных функций Kotlin: run, with, let, also и apply

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

Функции скоупа

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

Самый простой способ проиллюстрировать определение скоупа, это функция run

fun test() {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}

Logcat:
I/System.out: I am happy
I/System.out: I am sad

Благодаря этому, внутри функции test вы можете получить отдельный изолированный скоуп, в котором mood переопределен, чтобы напечатать "I am happy".

Эта функция скоупа сама по себе кажется не очень полезной. Но есть и еще один приятный момент, помимо скоупа; он возвращает что-то, то есть последний объект в области видимости.

Следовательно, мы можем "красиво" применить show() к одному из view, не вызывая его дважды:

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

3 атрибута скоуп функции

Чтобы сделать функции обзора более интересными, позвольте мне классифицировать их поведение по 3 атрибутам. Я буду использовать эти атрибуты, чтобы отличать их друг от друга.

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

Если мы посмотрим на 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 перед ее использованием.


2. this VS it

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

stringVariable?.run {
      println("The length of this String is $length")
}

// тоже самое что и
stringVariable?.let {
      println("The length of this String is ${it.length}")

}

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

Однако для сигнатуры функции T.let вы заметите, что T.let отправляет себя в функцию, т.е. в блок: (T). Следовательно, это похоже на лямбда-аргумент. Это можно отнести к функции скоупа как есть. Поэтому я называю это отправкой в качестве аргумента.

Из вышесказанного кажется, что T.run лучше, чем T.let, так как он неявен, но есть некоторые тонкие преимущества функции T.let, как показано ниже:
  • T.let обеспечивает более четкое разграничение использования данной переменной функции/члена по сравнению с функцией/членом внешнего класса
  • Если this нельзя пропустить, например, когда оно отправляется в качестве параметра функции, ее запись короче и понятнее
  • T.let позволяет лучше именовать преобразованную используемую переменную, то есть вы можете преобразовать ее в другое имя.


stringVariable?.let {

      nonNullString ->

          println("The non null string is $nonNullString")

}


3. Возвращение this VS другие типы

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

stringVariable?.let {
      println("The length of this String is ${it.length}")
}

// Точно тоже самое что и

stringVariable?.also {
      println("The length of this String is ${it.length}")
}

Однако их главное отличие - вот что они возвращают. T.let возвращает значение другого типа, в то время как T.also возвращает сам T, то есть this.

Оба полезны для цепочки функций, где помощь T.let позволяет вам расширить операцию, а T.also позволяет вам использовать ту же самую переменную (т.е. this) в следующем обработчике.

Простая иллюстрация ниже

val original = "abc"

// Обрабатываем значение и отправляем в следующий обработчик
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // can be evolve to other type
}.let {
    println("The length of the String is $it") // 3
}

// Неправильно
// Тоже самое значение в следующий обработчик (печатает неправильный ответ)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}

// Исправленный вариант also (т.е. манипуляции с исходной строкой)
// Тоже самое значение отправляется в следующий обработчик 
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3

}

T.also может показаться бессмысленным выше, так как мы могли бы легко объединить их в единый блок функций. Но если хорошо продумать, у него есть несколько хороших преимуществ:
  1. Можно очень четко разделить процесс обработки объекта на маленькие функциональные участки
  2. Подход очень мощный для манипуляций со значениями перед использованием, мы создаем цепочку Builder
Когда два действия объединяются в цепь, т.е. сначала вычисляется одно значение, другое действие ждет, это становится очень мощным подходом, например, ниже:


// Нормальный подход
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}

// Улучшенный подход
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }


Обзор всех свойств

Посмотрев на 3 свойства, мы в значительной степени знать поведение функции. Позвольте мне проиллюстрировать функцию T.apply, так как она не упоминалась выше. 3 атрибута T.apply, как показано ниже:

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

Следовательно, можно представить, что его можно использовать следующим образом:

// Нормальный подход
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}

// Улучшенный подход
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }

Или мы могли бы также сделать цепочку создания объектов:

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}

// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

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

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



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