В этой статье мы рассмотрим самые распространенные способы «магического» использования функций в 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 в Kotlin по аналогии могут быть и Extension Properties:
private val String.first: Char
get() {
return this[0]
}
print("Kotlin".first) // K
Большинство «магических» применений функций и лямбд в Kotlin, так же как и эта, являются не более чем синтаксическим сахаром, но каким удобным!
Лямбда-функции и анонимные функции
В 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()
и передаем ей в качестве аргумента лямбда-функцию, которую она вызовет позже. Koltin позволяет вынести лямбду за скобки, если она — последний аргументом функции, а также вовсе убрать скобки, если лямбда является единственным аргументом. Также в большинстве случаев можно убрать тип возвращаемого значения и заменить аргументы на _
, если они не используются. Вот более короткий вариант:
longWork { _, _ ->
print("callback runned")
}
Но можно и вовсе убрать аргументы у лямбда-функции, если они не нужны:
longWork {
print("callback runned")
}
И это внешне напоминает уже не функцию высшего порядка, а языковую конструкцию, как например synchronized
в Java. К слову, synchronized
в Kotlin построен именно как функция высшего порядка.
Это очень удобно для создания так называемых DSL (Domain-Specific Languages) – предметно-ориентированных языков. Одни из самых популярных DSL для Kotlin — Anko (Android UI прямо в Kotlin с сохранением удобства XML-разметки), Gradle Kotlin DSL (Gradle-скрипты на Kotlin), kotlinx.html (по аналогии с Anko).
Для примера рассмотрим 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 в том, что в отличие от декларативного HTML, в Kotlin есть переменные, которые могут быть использованы для генерации динамической страницы. И это намного красивее и удобнее классической генерации страницы через конкатенацию множества строк. В реальности для генерации 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
Более сложные лямбды
Рассмотрим пример кода:
val builder = StringBuilder()
builder.append("Hello")
builder.append("World")
builder.append("Kotlin")
builder.append("The Best")
print(builder.toString())
С помощью более сложных функций высшего порядка из стандартной библиотеки языка можно превратить код выше в следующее:
StringBuilder().apply {
append("Hello")
append("World")
append("Kotlin")
append("The Best")
}.let {
print(it.toString())
}
Как можно заметить, метод apply()
позволяет не писать несколько раз builder.append()
благодаря следующему прототипу метода:
public inline fun <T> T.apply(block: T.() -> Unit): T
Здесь лямбда-функция block
— метод-расширение для типа T
, в данном случае для StringBuilder
. И append()
внутри лямбды block
— это this.append()
, где this
– экземпляр класса StringBuilder
.
Метод let()
действует схожим образом, только принимает немного другую лямбду:
public inline fun <T, R> T.let(block: (T) -> R): R
Здесь ссылка на объект передается не в качестве this
, а в качестве явного аргумента метода, но мы его не указали. В таких случаях компилятор первый аргумент лямбда-функции автоматически называет «it».
Немного о недосказанном
Во-первых, в Kotlin, в отличие от Java, есть перегружаемые операторы. Так, например, если у класса есть метод plus()
, то его можно вызвать оператором +
, а метод get()
– оператором []
.
Во-вторых, функции в Kotlin могут быть явно помечены как 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 своими «магическими» функциями позволяет сильно упростить написание и, самое главное, чтение кода. Удобство написания и безопасность – это два самых важных отличия Kotlin от созданной ещё в 1995(!) году Java. В то время об удобстве и безопасности кода только мечтали.
Иван Борисов