Магия функций в Kotlin
Kotlin — простой и совместимый с Java язык программирования, который сокращает время написания кода за счет более коротких конструкций. В этой статье мы расскажем про несколько популярных способов «магического» применения функций в Kotlin.
Extension Functions
Первый способ — расширение классов без наследования. Он позволяет, не меняя класс String и использующие его пакеты, расширить данный класс путём добавления нового метода или свойства.
Представьте, что у нас есть метод
private fun String.deleteSpaces(): String { return this.replace(" ", "") }
Мы можем использовать этот метод таким образом, как будто он — это часть класса String. Пользователь увидит это вот так:
println("Hel lo, Wor ld".deleteSpaces()) // Hello,World
После выполнения компиляции этот метод уже будет выглядеть приблизительно так (код упрощён, главное — понять суть):
private static final String deleteSpaces(@NotNull String receiver) { return receiver.replace(" ", ""); }
Следовательно, делаем вывод, что внутри метода
Кроме Extension Functions, в Котлин по аналогии могут быть и Extension Properties:
private val String.first: Char get() { return this[0] } print("Kotlin".first) // K
Большая часть «магических» применений лямбд и функций в Котлин, по сути, являются не более, чем синтаксическим сахаром, но зато каким удобным!
Анонимные функции и лямбда-функции
Анонимные и лямбда-функции в Kotlin — это функции без имени, однако при этом их можно использовать в качестве объектов. И писать их можно непосредственно внутри выражения без отдельного объявления.
Рассмотрим синтаксис лямбда-выражения:
{ аргументы -> возвращаемый_тип тело_функции }
Что касается анонимной функции, то синтаксис её декларации абсолютно совпадает с синтаксисом обычной функции, просто у первой нет имени.
fun defaultFun(x: Int, y: Int): Int = x + y // Именованная функция val f = fun(x: Int, y: Int): Int = x + y // Анонимная функция
Теперь давайте посмотрим на использование лямбд совместно с функциями высшего порядка.
Функции высшего порядка
Речь идёт о функции, принимающей в виде одного из аргументов другую функцию, включая лямбду либо анонимную функцию. Ярчайший пример применения – callback.
Представьте, что у нас есть функция высшего порядка
private fun longWork(callback: (arg1: Int, arg2: String) -> Unit) { val result = doSomething() callback(result, "Kotlin > Java") // вызов callback }
Эта функция принимает в виде аргумента функцию
longWork({ arg1, arg2 -> Unit print("callback runned") })
Как видите, мы вызываем функцию
longWork { _, _ -> print("callback runned") }
Всё это внешне уже напоминает не функцию высшего порядка, а языковую конструкцию, как, к примеру synchronized в Java. Кстати, synchronized в Котлин построен именно как функция высшего порядка.
Всё это весьма удобно при создании DSL – предметно-ориентированных языков. Допустим, того же Anko (Android UI прямо в Kotlin с сохранением удобства XML-разметки), или kotlinx.html (по аналогии с Anko), или Gradle Kotlin DSL (Gradle-скрипты на Котлин).
Давайте посмотрим на HTML-страницу на Kotlin:
System.out.appendHTML().html { body { div { a("http://kotlinlang.org") { target = ATarget.blank +"Main site" } } } }
Вывод в stdout будет следующим:
<html> <body> <div><a href="http://kotlinlang.org" target="_blank">Main site</a></div> </body> </html>
Главный плюс этого DSL заключается в том, что в Kotlin существуют переменные, которые можно использовать для генерации динамической страницы (в отличие, скажем, от декларативного HTML). И всё это красивее и удобнее классической генерации страниц посредством конкатенации множества строк. Но вообще на практике для генерации HTML-разметки применяют иные методы, а этот мы показали лишь в качестве примера DSL в Kotlin.
Кроме того, возможно использование функций высшего порядка в качестве аналога Streams API из Java:
listOf(1, 2, 3, 4, 5) .filter { n -> n % 2 == 1 } // 1, 3, 5 .map { n -> n * n } // 1, 9, 25 .drop(1) // 9, 25 .take(1) // 9
Ещё несколько слов о Kotlin
В Kotlin, в отличие от Джавы, присутствуют перегружаемые операторы. К примеру, если у класса есть метод
Во-вторых, функции в Котлин можно пометить явно как inline либо noinline. Такой модификатор говорит компилятору, стоит ли заинлайнить функцию в целях повышения производительности либо нет. Но тут есть нюанс, связанный с разным поведением return в inline- и noinline-функциях.
В inline-функциях return будет произведен из ближайшей по области видимости noinline-функции. А в noinline – непосредственно из самой функции. Данную проблему решают «именованные return».
Чтобы сделать return из лямбды, которую мы передаём в вышеописанном примере в
Кстати, именованными могут быть не только return, но и break, continue. Кроме того, есть возможность создавать и свои метки:
loop@ for (i in 1..100) { for (j in 1..100) { if (...) break@loop } }
Также есть модификатор функции tailrec, который сообщает компилятору заменить рекурсию в функции на цикл, если функция написана в функциональном формате return if-then-else:
tailrec fun findFixPoint(x: Double = 1.0): Double = when { x == cos(x) -> x else -> findFixPoint(cos(x)) } // При компиляции будет заменена на fun findFixPoint(): Double { var x = 1.0 while (true) { val y = cos(x) if (x == y) return x x = y } }
В-третьих, в ситуации, когда метод требует в виде аргументов объект, который должен быть унаследован от класса/интерфейса с одним абстрактным методом, в эту функцию мы можем передать лямбду либо анонимную функцию, а компилятор самостоятельно создаст анонимный класс с переопределением абстрактного метода на нашу лямбду. К примеру, в стандартной Android-библиотеке существует метод
Кстати, лямбда, переданная в качестве
Выводы
Язык программирования Kotlin с помощью своих «магических» функций существенно упрощает написание и, что не менее важно, чтение кода. А удобство и безопасность – наиболее важные отличия Kotlin от созданной в далёком 1995 году Java, когда о безопасности и удобстве мы только мечтали.