Освой Kotlin играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
При работе с коллекциями нам приходилось иметь дело с обобщениями.
val cats = mutableListOf<Cat>()
Изучим их поподробнее, так как это важная тема.
Тип элемента определяется в угловых скобках <> — это означает, что он использует обобщения. Обобщения позволяют писать код, безопасный по отношению к типам. Это не позволяет поместить объект Dog (сраный пёсик) в список с элементами Cat. Компилятор знает, что в MutableList<Cat> можно добавлять только объекты Cat, а следовательно, на стадии компиляции будет выявляться большее количество проблем.
Если посмотреть в документации на определение MutableList, то увидим следующее.
interface MutableList<E> : List<E>, MutableCollection<E> {
fun add(index: Int, element: E): Unit
...
}
MutableList использует «E» (от слова Element) как условное обозначение типа элемента, который должен храниться и возвращаться коллекцией. Когда вы видите «E» в документации, мысленно подставьте на это место тот тип, который должен храниться в коллекции. Например, MutableList<String> означает, что «E» превращается в «String» в любой функции или объявлении переменной, в которых используется «E». А MutableList<Cat> означает, что все вхождения «E» превращаются в «Cat».
При таком подходе срабатывает защита от дурака. Если вы создадите MutableList с типом Cat, функция add() позволит добавлять только объекты Cat, но не пропустит объекты String и другие чужие объекты.
Можно создать функцию с обобщённым параметром:
fun meow(cats: MutableList<Cat>) {
//...
}
Можно создать функцию, которая возвращает обобщённый тип. Например, следующий код возвращает список MutableList с элементами Cat:
fun getCats(breed: String): MutableList<Cat> {
//Получение объектов Cat
}
Теперь подробнее о том, как пользоваться обобщениями. Создадим абстрактный класс Pet и три производных класса от него: Кот, Пёс и Рыбка.
abstract class Pet(var name: String)
class Cat(name: String) : Pet(name)
class Dog(name: String) : Pet(name)
class Fish(name: String) : Pet(name)
Также нам понадобится класс "Выставка", которая используется для проведения выставок разных типов животных с выдачей оценок и определения победителя. С его помощью мы будем управлять оценками участников для определения победителя. Ни разу не видел, чтобы одновременно проходила выставка котов и рыбок, поэтому логично ограничить класс только определённым типом животного. В выставке кошек участвуют только кошки, а в конкурсе рыбок — только рыбки. При определении класса Contest будут использоваться обобщения, чтобы ограничиться животными определённого типа.
class Contest<T> {
val scores: MutableMap<T, Int> = mutableMapOf()
fun addScore(t: T, score: Int = 0) {
if (score >= 0) scores.put(t, score)
}
fun getWinners(): MutableSet<T> {
val highScore = scores.values.max()
val winners: MutableSet<T> = mutableSetOf()
for ((t, score) in scores) {
if (score == highScore) winners.add(t)
}
return winners
}
}
Чтобы указать, что класс использует обобщённый тип, укажите имя типа в угловых скобках после имени класса. В данном случае обобщённый тип обозначается T. Считайте, что T заменяет реальный тип, который используется каждым отдельным объектом Contest. Вы можете использовать любую букву, но принято использовать в подобных случаях именно T. Кстати, если создаётся класс коллекции или интерфейс, то принято использовать E (сокращение от «Element»), а для ключей и значений массивов — K или V (сокращения от «Key» и «Value»).
Но нам не нужен слишком обобщённый тип, поэтому установим ограничение, указав нужный тип. Мы хотим, чтобы тип T был разновидностью Pet:
class Contest<T : Pet> {
...
}
Это означает, что мы можем создавать объекты Contest для хранения Cat, Fish или Pet, но не для объектов Ball или Flower.