Введение в Kotlin: коллекции, data-классы, деструктуризация и sealed-классы
Введение в Kotlin: функции, переменные, условия, циклы
Введение в Kotlin: классы, конструкторы, методы и свойства, наследование
Введение в Kotlin: интерфейсы, модификаторы доступа, вложенные классы, ключевые слова this и object
Это четвертая статья, входящая в цикл, посвященный введению в главные конструкции языка Kotlin. В этой части будут рассмотрены коллекции, data-классы, деструктуризация и sealed-классы. Подробнее о них можно почитать в официальной документации: коллекции, data-классы, деструктуризация и sealed-классы.
Коллекции
В отличие от многих языков, Kotlin различает изменяемые и неизменяемые коллекции (списки, множества, ассоциативные списки и т.д.). Важно понимать различие между read-only представлением изменяемой коллекции (пример ниже) и фактически неизменяемой коллекцией.
Прим. ред.: конструкции <T> и <out T> - это дженерики языка Kotlin, подробнее о них будет рассказано в одной из следующих статей этой серии.
Тип List<out T> в Kotlin — интерфейс, который предоставляет read-only операции, такие как size, get, и другие. Так же, как и в Java, он наследуется от Collection<T>, а значит и от Iterable<T>, поэтому его элементы могут быть прочитаны через iterator. Методы, которые изменяют список, добавлены в интерфейс MutableList<T>. То же самое относится и к Set<out T>/MutableSet<T>, Map<K, out V>/MutableMap<K, V>.
Пример базового использования списка (list) и множества (set):
val numbers: MutableList<Int> = mutableListOf(1, 2, 3) val readOnlyNumbers: List<Int> = numbers println(numbers) // выведет "[1, 2, 3]" numbers.add(4) println(readOnlyNumbers) // выведет "[1, 2, 3, 4]" readOnlyNumbers.clear() // не скомпилируется val strings = hashSetOf("a", "b", "c", "c") assert(strings.size == 3)
Для создания списков и множеств используйте методы из стандартной библиотеки, такие как listOf(), mutableListOf(), setOf(), mutableSetOf(). Создание ассоциативного списка может быть осуществлено с помощью простой идиомы: mapOf(a to b, c to d).
Заметьте, что содержимое переменной readOnlyNumbers изменяется вместе со списком, на который она указывает.
Иногда вам необходимо вернуть состояние коллекции в определённый момент времени:
class Controller { private val _items = mutableListOf<String>() val items: List<String> get() = _items.toList() }
Расширение toList просто копирует элементы списка. Таким образом, возвращаемый список гарантированно не изменится.
Существует несколько полезных расширений для списков и множеств, с которыми стоит познакомиться:
val items = listOf(1, 2, 3, 4) items.first() == 1 items.last() == 4 items.filter { it % 2 == 0 } // возвратит [2, 4] val rwList = mutableListOf(1, 2, 3) rwList.requireNoNulls() // возвратит [1, 2, 3] if (rwList.none { it > 6 }) println("Нет элементов больше 6") // выведет "Нет элементов больше 6" val item = rwList.firstOrNull()
Также обратите внимание на такие утилиты, как sort, zip, fold, reduce.
То же самое происходит и с ассоциативными списками. Они могут быть с лёгкостью инициализированы и использованы следующим образом:
val readWriteMap = hashMapOf("foo" to 1, "bar" to 2) println(readWriteMap["foo"]) // выведет "1" val snapshot: Map<String, Int> = HashMap(readWriteMap)
Data-классы
Нередко мы создаём классы, единственным назначением которых является хранение данных. Функционал таких классов зависит от самих данных, которые в них хранятся. В Kotlin класс может быть отмечен словом data:
data class User(val name: String, val age: Int)
Такой класс называется классом данных (data-классом). Компилятор автоматически извлекает все члены данного класса из свойств, объявленных в первичном конструкторе:
- пара функций equals() и hashCode(),
- toString() в форме "User(name=John, age=42)",
- функции componentN(), которые соответствуют свойствам, в зависимости от их порядка либо объявления,
- функция copy() (см. ниже)
Если какая-либо из этих функций явно определена в теле класса (или унаследована от родительского класса), то генерироваться она не будет.
Для того чтобы поведение генерируемого кода соответствовало здравому смыслу, классы данных должны быть оформлены с соблюдением некоторых требований:
- Первичный конструктор должен иметь как минимум один параметр.
- Все параметры первичного конструктора должны быть отмечены как val или var.
- Классы данных не могут быть абстрактными, open, sealed или inner.
Классы данных могут расширять другие классы (см. примеры ниже в блоке "Sealed-классы").
Data-классы, как и обычные классы, могут использовать свойства по-умолчанию:
data class User(val name: String = "", val age: Int = 0)
Довольно часто нам приходится копировать объект с изменением только некоторых его свойств. Для этой задачи генерируется функция copy(). Для написанного выше класса User такая реализация будет выглядеть следующим образом:
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
Это позволяет нам писать
val jack = User(name = "Jack", age = 1) val olderJack = jack.copy(age = 2)
Деструктуризация
Иногда удобно деструктуризировать объект на несколько переменных, например:
val (name, age) = person
Этот синтаксис называется деструктуризирующее присваивание. Он позволяет присвоить объект сразу нескольким переменным, разбив его на части. Мы объявили две переменные: name и age, и теперь можем использовать их по отдельности:
println(name) println(age)
Деструктуризирующие присваивания также работают в циклах for:
for ((a, b) in collection) { ... }
Предположим, нам нужно вернуть два значения из функции. Например, результат вычисления и какой-нибудь статус. Компактный способ достичь этого — объявление data-класса и возвращение его экземпляра:
data class Result(val result: Int, val status: Status) fun function(...): Result { // вычисления return Result(result, status) } // Теперь мы можем использовать деструктуризирующее присваивание: val (result, status) = function(...)
Так как data-классы автоматически реализуют методы componentN(), необходимые для деструктуризации (подробнее здесь), то она будет работать с ними "из коробки".
Примечание: мы также могли использовать стандартный класс Pair, чтобы заставить функцию вернуть Pair<Int, Status>, но правильнее будет именовать ваши данные должным образом.
Пожалуй, самый лучший способ итерации по ассоциативному списку:
for ((key, value) in map) { // do something with the key and the value }
Вы можете свободно использовать мульти-декларации в циклах for с ассоциативными списками, так же как и с коллекциями экземпляров data-классов.
Sealed-классы
Sealed-классы ("изолированные классы") используются для отражения ограниченных иерархий классов, когда значение может иметь тип только из ограниченного набора и никакой другой. Они являются, по сути, расширением enum-классов: набор значений enum типа также ограничен, но каждая enum-константа существует только в единственном экземпляре, в то время как наследник изолированного класса может иметь множество экземпляров, которые могут нести в себе какое-то состояние.
Чтобы описать изолированный класс, укажите модификатор sealed перед именем класса. Изолированный класс может иметь наследников, но все они должны быть объявлены в том же файле, что и сам sealed-класс, или внутри sealed-класса.
sealed class Expr data class Const(val number: Double) : Expr() data class Sum(val e1: Expr, val e2: Expr) : Expr() object NotANumber : Expr() fun eval(expr: Expr): Double = when (expr) { is Const -> expr.number is Sum -> eval(expr.e1) + eval(expr.e2) NotANumber -> Double.NaN }
Обратите внимание, что классы, которые расширяют наследников изолированного класса (непрямые наследники), могут быть помещены где угодно, не обязательно в том же файле.
Ключевое преимущество от использования изолированных классов проявляется тогда, когда вы используете их в выражении when. Если возможно проверить, что выражение покрывает все случаи, то вам не нужно добавлять else.
fun eval(expr: Expr): Double = when(expr) { is Expr.Const -> expr.number is Expr.Sum -> eval(expr.e1) + eval(expr.e2) Expr.NotANumber -> Double.NaN // оператор `else` не требуется, потому что мы покрыли все возможные случаи }
Продолжение серии статей "Введение в Kotlin" доступно здесь.
Источники:
Документация Kotlin: Коллекции
Документация Kotlin: Классы данных