Некоторые стандартные функции Kotlin настолько похожи, что мы не уверены, какую из них использовать. Здесь я представлю простой способ, чтобы четко их различать и выбирать, какую использовать.
Самый простой способ проиллюстрировать определение скоупа, это функция 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()
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 перед ее использованием.
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 и 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 может показаться бессмысленным выше, так как мы могли бы легко объединить их в единый блок функций. Но если хорошо продумать, у него есть несколько хороших преимуществ:
Функции скоупа
Функции, на которых я сконцентрируюсь, это 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 другие типы
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 может показаться бессмысленным выше, так как мы могли бы легко объединить их в единый блок функций. Но если хорошо продумать, у него есть несколько хороших преимуществ:
- Можно очень четко разделить процесс обработки объекта на маленькие функциональные участки
- Подход очень мощный для манипуляций со значениями перед использованием, мы создаем цепочку 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 атрибутов мы теперь можем классифицировать функции. И на основании этого мы могли бы сформировать дерево решений ниже, которое могло бы помочь решить, какую функцию мы хотим использовать, ожидая того, что нам нужно.
Надеюсь что эта блок-схема проясняет функции, а также упрощает процесс принятия решений, позволяя вам надлежащим образом освоить использование этих функций.