Взгляните на некоторые интересные и часто упускаемые из виду функции
Заявления о разрушении
Деструктурирующие объявления позволяют нам извлекать поля из объекта, аналогично тому, что можно сделать в 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 class
es.
Фактически, с копией Kotlin на data class
es вы можете изменять более одного свойства за раз. Все остальные свойства будут использовать значения, установленные в исходном объекте.
Вот пример:
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. Не забудьте также ознакомиться с ссылкой на Котлин. Есть много тем, которые я здесь не затронул, например сопрограммы, которые там подробно объясняются.
Спасибо за прочтение!