Взгляните на некоторые интересные и часто упускаемые из виду функции

Заявления о разрушении

Деструктурирующие объявления позволяют нам извлекать поля из объекта, аналогично тому, что можно сделать в TypeScript / ES6.

Представьте, что у нас есть следующее data class:

data class Person(val name: String, val age: Int)

Можно деструктурировать поля name и age на две соответствующие переменные в одной инструкции:

val john = Person("John Doe", 23)
val (name, age) = john
println(name) // John Doe
println(age) // 23

Однако есть кое-что, о чем нужно помнить. Имена переменных не имеют абсолютно никакого отношения к процессу. Важно их положение относительно полей объекта. Это может быть проблематично, если вы перемещаете положение некоторых полей data class и используете деструктуризацию. Если вы добавите новое поле, это тоже может вызвать проблемы.

// before
data class Person(val name: String, val age: Int)
val jane = Person("Jane Doe", 23)
val (name, age) = jane
// after
data class Person(
  val firstName: String,
  val lastName: String,
  val age: Int
)
val (name, age) = jane // this will now be firstName and lastName

Если вы используете TDD, ваши тесты должны выявить эти проблемы за вас. В любом случае лучше быть осторожным.

Деструктурирование также можно использовать для лямбда-параметра:

val people = listOf(
   Person("John Doe", 23),
   Person("Jane Doe", 21),
   ...
)
people.forEach { (name, age) -> ... }

По умолчанию это можно использовать для пар, map записей и многих других типов:

map.forEach { (key, value) -> ... }
val (x, y) = Pair(1, 2)

По умолчанию простые классы не поддерживают деструктуризацию. Его можно включить для них, добавив componentN методов, где N - позиция деструктуризации. Например:

class Person(val name: String, val age: Int) {
  operator fun component1() = name
  operator fun component2() = age
}

Число после component представляет позицию, в которой значение окажется после деструктуризации объекта.

Вам не нужно каждый раз деструктурировать все поля. Вы можете деструктурировать только первые поля, которые вам нужны. Вы также можете игнорировать поля, используя символ _. Например:

data class Person(
   val firstName: String,
   val lastName: String,
   val age: Int
)
val john = Person("John", "Doe", 23)
val (firstName, lastName) = john
val (_, _, age) = john

Перегрузка оператора

Kotlin поддерживает перегрузку операторов. В некоторых классах уже есть перегруженные операторы. Например:

val result = listOf(1, 2, 3) + listOf(4, 5, 6)
println(result) // 1, 2, 3, 4, 5, 6
val duration = Duration.ofSeconds(10) + Duration.ofSeconds(5)
println(duration) // PT15S (15 seconds)

Вы можете перегрузить операторы для своих классов, объявив operator методы. Полный список доступных операторов доступен в официальной документации. В качестве простого примера давайте рассмотрим оператор plus:

data class Quantity(val value: Int) {
    operator fun plus(other: Quantity) =
       Quantity(value + other.value)
}

other не обязательно должен быть одного типа - он может быть любого типа. Теперь мы можем написать:

val a = Quantity(10)
val b = Quantity(15)
val c = a + b // Quantity(25)

Еще одна интересная вещь - вы можете использовать расширения для добавления операторов к существующим классам:

operator fun Quantity.plus(quantity: Int) =
   Quantity(value + quantity)
val a = Quantity(10) + 15 // Quantity(25)

Сопоставимый

Любой класс, реализующий Comparable, автоматически поставляется со следующими операторами сравнения: <, >, <= и >=:

data class Quantity(val value: Int) : Comparable<Quantity> {
    override fun compareTo(other: Quantity) =
            value.compareTo(other.value)
}
// now we can compare:
val a = Quantity(10)
val b = Quantity(5)
if (a >= b) ...

Копировать

У вас когда-нибудь был огромный объект передачи данных или объект значения, у которого была масса свойств, и вы хотели создать его копию с измененным только одним свойством? Kotlin предоставляет эту функцию "из коробки" для data classes.

Фактически, с копией Kotlin на data classes вы можете изменять более одного свойства за раз. Все остальные свойства будут использовать значения, установленные в исходном объекте.

Вот пример:

data class Person(
    val firstName: String,
    val lastName: String,
    val age: Int
)
val john = Person("John", "Doe", 23)
val jane = john.copy(
  firstName = "Jane",
  age = 21
) // Jane Doe, 21

Обозначение инфиксов

Даже если вы не знаете инфиксное ключевое слово , вы наверняка уже использовали его раньше. Например, "a" to 10 для создания пары и 0 until 10 для создания диапазона используют инфиксную нотацию. Котест предлагает инфиксные обозначения своих утверждений, что делает их приятными для чтения: something shouldBe someValue.

Обозначение infix позволяет вам опускать точку и круглые скобки при вызове функции. Например, приведенные выше примеры эквивалентны:

"a" to 10 == "a".to(10)
0 until 10 == 0.until(10)
something shouldBe someValue == something.shouldBe(someValue)

Чтобы объявить функцию infix, у нее должен быть только один параметр, не имеющий значения по умолчанию, и он должен быть членом или функцией расширения.

enum class CoffeeType { espresso }
class CoffeeMachine {
    infix fun make(coffee: CoffeeType) {
       // ...
    }
}
val coffeeMachine = CoffeeMachine()
coffeeMachine make espresso

Вызовы Infix могут быть объединены в цепочку в зависимости от их типов возврата. Вот что позволяет нам писать:

0 until 10 step 2

Reified

Ключевое слово reified позволяет получить доступ к типу класса без необходимости указывать его в качестве параметра. Это применимо только к inline методам и не использует никакого отражения. Это не то, что вам понадобится очень часто, но может пригодиться при работе с типами классов.

Вот что использует MockK при имитации классов:

val service = mockk<Service>()
// instead of having to write
val service = mockk(Service::class.java)

Ниже приводится пример, с которым я столкнулся, где метод reified использовался для создания пары лямбда-выражений типа-создатель для фабрики. Фабрика должна была вызвать другую лямбду-создатель в зависимости от типа класса:

inline fun <reified T> factory(noinline creator: () -> T) =
    T::class.java to creator
val factoriesByClass = mapOf(
    factory { Person("John Doe", 23) },
    factory { CoffeMachine() }
)
// instead of 
val factoriesByClass = mapOf(
    Person::class.java to { Person("John Doe", 23) },
    CoffeMachine::class.java to { CoffeMachine() }
)

Это сделало фабричный код намного более читабельным, поскольку он не был загрязнен избыточными параметрами типа класса.

Тип Псевдоним

Когда вы начинаете получать сложную комбинацию нескольких типов, такую ​​как Map<String, List<Person>> (я уверен, что вы сталкивались с более сложной комбинацией), печатать ее каждый раз может быть довольно утомительно. К счастью, в Kotlin есть псевдонимы типов, которые позволяют упростить эти типы и сделать их более выразительными:

typealias Registry = Map<String, List<Person>>

Эту функцию тоже можно использовать просто для выразительности:

typealias Position = Pair<Int, Int>

Делегированные свойства

Делегированные свойства позволяют добавлять логику к свойствам без необходимости кодировать эту логику вручную внутри того же класса. Вы можете написать своих собственных делегатов. Таким образом, логику делегата можно изолировать и протестировать независимо от бизнес-логики. Это очень хорошо объяснено в официальной документации. Вы также можете использовать некоторые стандартные делегаты.

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

val lazyValue: SomethingExpensiveToCreate by lazy {
    SomethingExpensiveToCreate()
}

Другой стандартный, Observable, позволяет отслеживать любые изменения, внесенные в поле, путем предоставления лямбда-выражения, которое вызывается со старым и новым значением.

Поскольку вы можете реализовать свои собственные делегаты, возможности безграничны.

Заключение

Надеюсь, вы найдете некоторые из этих функций полезными, если вы еще не знали о них. Отличный ресурс для получения дополнительной информации о Kotlin - это плагин EduTools. Не забудьте также ознакомиться с ссылкой на Котлин. Есть много тем, которые я здесь не затронул, например сопрограммы, которые там подробно объясняются.

Спасибо за прочтение!