Эта практическая работа является частью курса Kotlin Bootcamp for Programmers . Вы получите максимальную пользу от этого курса, если будете выполнять задания последовательно. В зависимости от вашего уровня знаний, вы можете пропустить некоторые разделы. Этот курс ориентирован на программистов, владеющих объектно-ориентированным языком программирования и желающих изучить Kotlin .
Введение
В этой лабораторной работе вы познакомитесь с универсальными классами, функциями и методами, а также с тем, как они работают в Kotlin.
Вместо создания одного примера приложения уроки этого курса направлены на углубление ваших знаний, но при этом они частично независимы друг от друга, чтобы вы могли бегло просмотреть знакомые разделы. Чтобы связать их воедино, многие примеры используют тему аквариума. А если вы хотите узнать всё об аквариуме, ознакомьтесь с курсом Kotlin Bootcamp for Programmers на Udacity.
Что вам уже следует знать
- Синтаксис функций, классов и методов Kotlin
- Как создать новый класс в IntelliJ IDEA и запустить программу
Чему вы научитесь
- Как работать с универсальными классами, методами и функциями
Что ты будешь делать?
- Создайте универсальный класс и добавьте ограничения
- Создайте типы
inиout - Создание универсальных функций, методов и функций расширения
Введение в дженерики
В Kotlin, как и во многих других языках программирования, есть обобщённые типы. Обобщённый тип позволяет сделать класс обобщённым, тем самым значительно более гибким.
Представьте, что вы реализуете класс MyList , содержащий список элементов. Без дженериков вам пришлось бы реализовать новую версию MyList для каждого типа: одну для Double , одну для String и одну для Fish . С дженериками вы можете сделать список дженериком, чтобы он мог содержать объекты любого типа. Это как сделать тип подстановочным знаком, который подходит ко многим типам.
Чтобы определить универсальный тип, поместите T в угловые скобки <T> после имени класса. (Можно использовать другую букву или более длинное имя, но общепринятым соглашением для универсального типа является T.)
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
} Вы можете ссылаться на T , как на обычный тип. Возвращаемый тип функции get() — T , а параметр функции addItem() — T . Конечно, универсальные списки очень полезны, поэтому класс List встроен в Kotlin.
Шаг 1: Создайте иерархию типов
На этом этапе вы создаёте несколько классов для использования на следующем этапе. Создание подклассов рассматривалось в предыдущей лабораторной работе, но вот краткий обзор.
- Чтобы не перегружать пример, создайте новый пакет в src и назовите его
generics. - В пакете generics создайте новый файл
Aquarium.kt. Это позволит вам переопределять элементы, используя те же имена, без конфликтов, поэтому весь остальной код для этой лабораторной работы будет помещен в этот файл. - Создайте иерархию типов водоснабжения. Для начала сделайте класс
WaterSupplyopen, чтобы его можно было подклассифицировать. - Добавьте логический параметр
varneedsProcessing. Это автоматически создаст изменяемое свойство, а также методы получения и установки. - Создайте подкласс
TapWater, расширяющийWaterSupply, и передайтеtrueдляneedsProcessing, поскольку водопроводная вода содержит добавки, вредные для рыб. - В
TapWaterопределите функциюaddChemicalCleaners(), которая устанавливаетneedsProcessingв значениеfalseпосле очистки воды. СвойствоneedsProcessingможно задать изTapWater, поскольку оно по умолчаниюpublicи доступно подклассам. Вот готовый код.
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}- Создайте ещё два подкласса класса
WaterSupply:FishStoreWaterиLakeWater.FishStoreWaterне требует обработки, ноLakeWaterнеобходимо отфильтровать с помощью методаfilter(). После фильтрации повторная обработка не требуется, поэтому вfilter()установитеneedsProcessing = false.
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}Если вам нужна дополнительная информация, просмотрите предыдущий урок о наследовании в Kotlin.
Шаг 2: Создайте универсальный класс
На этом этапе вы измените класс Aquarium для поддержки различных типов водоснабжения.
- В Aquarium.kt определите класс
Aquarium, поместив<T>в скобки после имени класса. - Добавьте неизменяемое свойство
waterSupplyтипаTвAquarium.
class Aquarium<T>(val waterSupply: T)- Напишите функцию
genericsExample(). Она не является частью класса, поэтому может располагаться на верхнем уровне файла, как функцияmain()или определения классов. В этой функции создайтеAquariumи передайте емуWaterSupply. Поскольку параметрwaterSupplyявляется универсальным, необходимо указать тип в угловых скобках<>.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}- В
genericsExample()ваш код может получить доступwaterSupplyаквариума. Поскольку он относится к типуTapWater, вы можете вызватьaddChemicalCleaners()без приведения типов.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- При создании объекта
Aquariumможно удалить угловые скобки и всё, что между ними, поскольку в Kotlin реализовано выведение типов. Поэтому нет смысла дважды указыватьTapWaterпри создании экземпляра. Тип можно вывести с помощью аргументаAquarium; он всё равно создастAquariumтипаTapWater.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- Чтобы увидеть, что происходит, выведите
needsProcessingдо и после вызоваaddChemicalCleaners(). Ниже представлен готовый код функции.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
aquarium.waterSupply.addChemicalCleaners()
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}- Добавьте функцию
main()для вызоваgenericsExample(), затем запустите программу и посмотрите на результат.
fun main() {
genericsExample()
}⇒ water needs processing: true water needs processing: false
Шаг 3: Сделайте это более конкретным
Обобщённый тип означает, что можно передать практически всё, что угодно, и иногда это проблема. На этом этапе вы конкретизируете класс Aquarium , указывая, что именно можно в него поместить.
- В
genericsExample()создайтеAquarium, передав строку дляwaterSupply, затем выведите свойствоwaterSupplyаквариума.
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}- Запустите программу и посмотрите на результат.
⇒ string
Результатом будет переданная вами строка, поскольку Aquarium не накладывает никаких ограничений на T. Можно передать любой тип, включая String .
- В
genericsExample()создайте ещё одинAquarium, передав значениеnullдляwaterSupply. Если значениеwaterSupplyравно null, выведите"waterSupply is null".
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}- Запустите программу и посмотрите на результат.
⇒ waterSupply is null
Почему можно передать null при создании Aquarium ? Это возможно, поскольку по умолчанию T обозначает тип Any? который допускает значение NULL, — тип, находящийся на вершине иерархии типов. Следующий код эквивалентен тому, что вы ввели ранее.
class Aquarium<T: Any?>(val waterSupply: T)- Чтобы не допустить передачу
null, явно сделайтеTтипомAny, удалив?послеAny.
class Aquarium<T: Any>(val waterSupply: T)В этом контексте Any называется общим ограничением . Это означает, что для T может быть передан любой тип, при условии, что он не равен null .
- На самом деле вам нужно убедиться, что для
Tможет быть передан толькоWaterSupply(или один из его подклассов). ЗаменитеAnyнаWaterSupply, чтобы определить более конкретное общее ограничение.
class Aquarium<T: WaterSupply>(val waterSupply: T)Шаг 4: Добавьте больше проверок
На этом этапе вы узнаете о функции check() , которая поможет вам убедиться, что ваш код работает ожидаемым образом. Функция check() — это стандартная библиотечная функция Kotlin. Она действует как утверждение и выдаёт исключение IllegalStateException , если её аргумент принимает значение false .
- Добавьте метод
addWater()к классуAquariumдля добавления воды с методомcheck()который гарантирует, что вам не нужно предварительно обрабатывать воду.
class Aquarium<T: WaterSupply>(val waterSupply: T) {
fun addWater() {
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
println("adding water from $waterSupply")
}
}В этом случае, если needsProcessing имеет значение true, check() выдаст исключение.
- В
genericsExample()добавьте код для созданияAquariumсLakeWater, а затем добавьте в него немного воды.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}- Запустите программу, и вы получите исключение, поскольку воду сначала нужно отфильтровать.
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first
at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)- Добавьте вызов для фильтрации воды перед добавлением её в
Aquarium. Теперь при запуске программы не будет генерироваться исключение.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}⇒ adding water from generics.LakeWater@880ec60
Вышеизложенное охватывает основы дженериков. Следующие задачи охватывают более подробную информацию, но важная концепция заключается в том, как объявить и использовать дженерик-класс с дженериком-ограничением.
В этом задании вы изучите входные и выходные типы данных с использованием дженериков. in тип — это тип, который может быть передан только в класс, но не возвращён. out тип — это тип, который может быть возвращён только из класса.
Взгляните на класс Aquarium , и вы увидите, что универсальный тип возвращается только при получении свойства waterSupply . В нём нет методов, принимающих значение типа T в качестве параметра (за исключением определения в конструкторе). Kotlin позволяет определить out -типы именно для этого случая и вывести дополнительную информацию о том, где эти типы безопасно использовать. Аналогично, вы можете определить in типы для универсальных типов, которые всегда передаются в методы, а не возвращаются. Это позволяет Kotlin выполнять дополнительные проверки безопасности кода.
Типы in и out — это директивы системы типов Kotlin. Описание всей системы типов выходит за рамки этого учебного курса (она довольно сложная); однако компилятор будет помечать типы, которые не помечены как in и out соответствующим образом, поэтому вам необходимо знать о них.
Шаг 1: Определите тип выхода
- В классе
AquariumизменитеT: WaterSupplyнаoutтип.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}- В том же файле, вне класса, объявите функцию
addItemTo(), которая ожидаетAquariumWaterSupply.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")- Вызовите
addItemTo()изgenericsExample()и запустите свою программу.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}⇒ item added
Kotlin может гарантировать, что addItemTo() не сделает ничего небезопасного с универсальным WaterSupply , поскольку он объявлен как out тип.
- Если удалить ключевое слово
out, компилятор выдаст ошибку при вызовеaddItemTo(), поскольку Kotlin не может гарантировать, что вы не делаете ничего небезопасного с типом.
Шаг 2: Определите тип in
Тип in аналогичен типу out , но предназначен для универсальных типов, которые передаются только в функции, а не возвращаются. При попытке вернуть тип in возникнет ошибка компиляции. В этом примере тип in будет определён как часть интерфейса.
- В Aquarium.kt определите интерфейс
Cleaner, который принимает универсальныйT, ограниченныйWaterSupply. Поскольку он используется только как аргумент дляclean(), вы можете сделать егоinпараметром.
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}- Чтобы использовать интерфейс
Cleaner, создайте классTapWaterCleaner, который реализуетCleanerдля очисткиTapWaterпутем добавления химикатов.
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}- В классе
AquariumобновитеaddWater()так, чтобы он принималCleanerтипаTи очищал воду перед ее добавлением.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
fun addWater(cleaner: Cleaner<T>) {
if (waterSupply.needsProcessing) {
cleaner.clean(waterSupply)
}
println("water added")
}
}- Обновите код примера
genericsExample(), чтобы создатьTapWaterCleaner—AquariumсTapWater— и добавить немного воды с помощью очистителя. Он будет использовать очиститель по мере необходимости.
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}Kotlin будет использовать информацию о типах in и out , чтобы гарантировать безопасное использование дженериков в вашем коде. Out и in легко запомнить: типы out можно передавать наружу как возвращаемые значения, а типы in — внутрь как аргументы.

Если вы хотите подробнее разобраться в том, какие проблемы решают типы in и out , документация подробно описывает их.
В этом задании вы узнаете о функциях-обобщениях и о том, когда их следует использовать. Как правило, создание функции-обобщения — это хорошая идея, когда функция принимает аргумент класса с обобщённым типом.
Шаг 1: Создание универсальной функции
- В generics/Aquarium.kt создайте функцию
isWaterClean(), которая принимаетAquarium. Необходимо указать универсальный тип параметра; один из вариантов — использоватьWaterSupply.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}Но это означает, что для вызова этой функции Aquarium должен быть параметр типа out . Иногда out или in слишком строгие, поскольку требуется использовать тип как для входных, так и для выходных данных. Требование out можно устранить, сделав функцию универсальной.
- Чтобы сделать функцию универсальной, добавьте угловые скобки после ключевого слова
funс универсальным типомTи любыми ограничениями, в данном случаеWaterSupply. Измените ограничениеAquariumтак, чтобы оно ограничивалось значениемTвместоWaterSupply.
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}T — это параметр типа для isWaterClean() , который используется для указания общего типа аквариума. Этот шаблон очень распространён, и стоит уделить ему немного времени и разобраться в нём.
- Вызовите функцию
isWaterClean()указав тип в угловых скобках сразу после имени функции и перед круглыми скобками.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}- Из-за вывода типа из аргумента
aquariumэтот тип не нужен, поэтому удалите его. Запустите программу и посмотрите на вывод.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}⇒ aquarium water is clean: false
Шаг 2: Создайте универсальный метод с материализованным типом
Вы также можете использовать универсальные функции для методов, даже в классах с собственным универсальным типом. На этом этапе вы добавляете в Aquarium универсальный метод, который проверяет, принадлежит ли объект типу WaterSupply .
- В классе
Aquariumобъявите методhasWaterSupplyOfType(), который принимает универсальный параметрR(Tуже используется), ограниченныйWaterSupply, и возвращаетtrueеслиwaterSupplyимеет типRОн похож на функцию, объявленную ранее, но внутри классаAquarium.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R- Обратите внимание, что последняя
Rподчеркнута красным. Наведите на неё указатель мыши, чтобы увидеть ошибку.
- Чтобы выполнить проверку
is, необходимо сообщить Kotlin, что тип является reified (реальным) и может быть использован в функции. Для этого добавьтеinlineперед ключевым словомfunиreifiedin перед обобщенным типомR.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R После того, как тип материализован, его можно использовать как обычный тип, поскольку после встраивания он становится реальным типом. Это означает, is с его помощью можно выполнять проверки.
Если вы не используете здесь reified , тип не будет достаточно «реальным», чтобы Kotlin допускал проверки is . Это связано с тем, что не овеществленные типы доступны только во время компиляции и не могут использоваться во время выполнения программы. Подробнее об этом говорится в следующем разделе.
- Передайте
TapWaterв качестве типа. Как и при вызове универсальных функций, для вызова универсальных методов используйте угловые скобки с указанием типа после имени функции. Запустите программу и посмотрите на результат.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}⇒ true
Шаг 3: Создание функций расширения
Вы также можете использовать вещественные типы для обычных функций и функций расширения.
- За пределами класса
Aquariumопределите функцию расширения дляWaterSupplyс именемisOfType(), которая проверяет, имеет ли переданныйWaterSupplyопределенный тип, например,TapWater.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T- Вызовите функцию расширения так же, как метод.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}⇒ true
С этими функциями расширения неважно, какой тип Aquarium используется ( Aquarium , TowerTank или какой-либо другой подкласс), главное, чтобы это был Aquarium . Использование синтаксиса звёздной проекции — удобный способ указать множество соответствий. Кроме того, при использовании звёздной проекции Kotlin гарантирует, что вы не сделаете ничего опасного.
- Чтобы использовать проекцию звёзд, добавьте
<*>послеAquarium. ПереведитеhasWaterSupplyOfType()в функцию расширения, поскольку она не является частью основного APIAquarium.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R- Измените вызов на
hasWaterSupplyOfType()и запустите свою программу.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}⇒ true
В предыдущем примере вам пришлось пометить универсальный тип как reified и сделать функцию inline , поскольку Kotlin должен знать о них во время выполнения, а не только во время компиляции.
Все универсальные типы используются Kotlin только во время компиляции. Это позволяет компилятору убедиться в безопасности выполнения. К моменту выполнения все универсальные типы удаляются, отсюда и предыдущее сообщение об ошибке проверки удалённого типа.
Оказывается, компилятор может создавать корректный код, не сохраняя обобщённые типы до времени выполнения. Но это означает, что иногда вы делаете что-то, is проверяете обобщённые типы, что компилятор не может поддержать. Именно поэтому в Kotlin появились овеществлённые, или вещественные, типы.
Подробнее о конкретизированных типах и стирании типов можно прочитать в документации Kotlin.
В этом уроке мы сосредоточились на обобщениях, которые важны для того, чтобы сделать код более гибким и простым для повторного использования.
- Создавайте универсальные классы, чтобы сделать код более гибким.
- Добавьте общие ограничения, чтобы ограничить типы, используемые с общими типами.
- Используйте
inиoutтипы с обобщенными типами, чтобы обеспечить лучшую проверку типов и ограничить типы, передаваемые в классы или возвращаемые из них. - Создайте универсальные функции и методы для работы с универсальными типами. Например:
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... } - Используйте общие функции расширения для добавления в класс дополнительных функций.
- Конкретизированные типы иногда необходимы из-за стирания типов. Конкретизированные типы, в отличие от универсальных, сохраняются во время выполнения.
- Используйте функцию
check(), чтобы убедиться, что ваш код работает как надо. Например:
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Документация Kotlin
Если вам нужна дополнительная информация по какой-либо теме этого курса или вы застряли, лучшей отправной точкой будет https://kotlinlang.org .
- Дженерики
- Общие ограничения
- Звездные проекции
- Типы
Inиout - Реифицированные параметры
- Стирание типа
- функция
check()
Учебники по Kotlin
На сайте https://try.kotlinlang.org вы найдете подробные учебные пособия по Kotlin Koans, веб-интерпретатор и полный набор справочной документации с примерами.
Курс Udacity
Чтобы просмотреть курс Udacity по этой теме, см. Kotlin Bootcamp for Programmers .
IntelliJ IDEA
Документацию по IntelliJ IDEA можно найти на сайте JetBrains.
В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:
- При необходимости задавайте домашнее задание.
- Объясните учащимся, как следует сдавать домашние задания.
- Оцените домашние задания.
Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.
Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.
Ответьте на эти вопросы
Вопрос 1
Какое из следующих соглашений является соглашением об именовании универсального типа?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
Вопрос 2
Ограничение на типы, допустимые для универсального типа, называется:
▢ общее ограничение
▢ общее ограничение
▢ разрешение неоднозначности
▢ ограничение универсального типа
Вопрос 3
Овеществление означает:
▢ Рассчитано реальное влияние исполнения объекта.
▢ Для класса установлен ограниченный индекс записи.
▢ Параметр универсального типа преобразован в действительный тип.
▢ Сработал удаленный индикатор ошибки.
Перейти к следующему уроку:
Обзор курса, включая ссылки на другие практические занятия, см. в статье «Kotlin Bootcamp for Programmers: Welcome to the course».