Магия функций в Kotlin

Kotlin — простой и совместимый с Java язык программирования, который сокращает время написания кода за счет более коротких конструкций. В этой статье мы расскажем про несколько популярных способов «магического» применения функций в Kotlin.

Extension Functions

Первый способ — расширение классов без наследования. Он позволяет, не меняя класс String и использующие его пакеты, расширить данный класс путём добавления нового метода или свойства.

Представьте, что у нас есть метод deleteSpaces():

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(" ", "");
}

Следовательно, делаем вывод, что внутри метода deleteSpaces() у нас присутствует доступ лишь к публичным полям и методам класса, в результате чего инкапсуляция не нарушается.

Кроме 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.

Представьте, что у нас есть функция высшего порядка longWork():

private fun longWork(callback: (arg1: Int, arg2: String) -> Unit) {
    val result = doSomething()
    callback(result, "Kotlin > Java") // вызов callback
}

Эта функция принимает в виде аргумента функцию callback(), однако вызывает её лишь после функции doSomething():

longWork({ arg1, arg2 -> Unit
    print("callback runned")
})

Как видите, мы вызываем функцию longWork(), передавая ей в виде аргумента лямбда-функцию, которую она вызовет позже. Язык программирования Котлин даёт возможность выносить лямбду за скобки в том случае, если она является последним аргументом функции, а также вообще убирать скобки, если лямбда — единственный аргумент. Кроме этого, чаще всего мы можем убрать тип возвращаемого значения, заменив аргументы на _ в том случае, если они не используются. Вот как выглядит укороченный вариант кода:

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, в отличие от Джавы, присутствуют перегружаемые операторы. К примеру, если у класса есть метод plus(), мы можем вызвать его оператором +, а метод get() – с помощью оператора []. Это во-первых.

Во-вторых, функции в Котлин можно пометить явно как inline либо noinline. Такой модификатор говорит компилятору, стоит ли заинлайнить функцию в целях повышения производительности либо нет. Но тут есть нюанс, связанный с разным поведением return в inline- и noinline-функциях.

В inline-функциях return будет произведен из ближайшей по области видимости noinline-функции. А в noinline – непосредственно из самой функции. Данную проблему решают «именованные return».

Чтобы сделать return из лямбды, которую мы передаём в вышеописанном примере в apply(), мы можем задействовать return@apply.

Кстати, именованными могут быть не только 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-библиотеке существует метод public void setOnClickListener(View.OnClickListener l), в котором OnClickListener представляет собой интерфейс с единственным методом onClick(View v).

Кстати, лямбда, переданная в качестве setOnClickListener { doSomething() }, скомпилируется в анонимный класс, реализующий интерфейс OnClickListener, в котором лямбда превратится в метод onClick(View v).

Выводы

Язык программирования Kotlin с помощью своих «магических» функций существенно упрощает написание и, что не менее важно, чтение кода. А удобство и безопасность – наиболее важные отличия Kotlin от созданной в далёком 1995 году Java, когда о безопасности и удобстве мы только мечтали.

Источник