Эта лабораторная работа является частью курса 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
. Это позволяет вам переопределять вещи, используя одни и те же имена, без конфликтов, поэтому остальная часть вашего кода для этой лаборатории кода помещается в этот файл. - Составьте типовую иерархию типов водоснабжения. Начните с того, что
WaterSupply
open
классом, чтобы он мог быть подклассом. - Добавьте логический параметр
var
,needsProcessing
. Это автоматически создает изменяемое свойство вместе с геттером и сеттером. - Создайте подкласс
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
.
- Что вы действительно хотите, так это убедиться, что только
WaterSupply
(или один из его подклассов) может быть передан дляT
. Замените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()
, которая ожидаетAquarium
ofWaterSupply
.
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
похож на тип 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
можно передавать внутрь как аргументы.
Если вы хотите больше узнать о проблемах в типах и решениях вне типов , документация подробно описывает их.
В этом задании вы узнаете об общих функциях и о том, когда их использовать. Как правило, создание универсальной функции является хорошей идеей, когда функция принимает аргумент класса, имеющего универсальный тип.
Шаг 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
иreified
перед универсальным типом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" }
Котлин документация
Если вам нужна дополнительная информация по какой-либо теме этого курса или если вы застряли, https://kotlinlang.org — лучшая отправная точка.
- Дженерики
- Общие ограничения
- Звездные проекции
- Типы
In
иout
- Овеществленные параметры
- Введите стирание
- функция
check()
Учебники по Котлину
На веб-сайте https://try.kotlinlang.org есть подробные руководства под названием Kotlin Koans, веб-интерпретатор и полный набор справочной документации с примерами.
Удасити курс
Чтобы просмотреть курс Udacity по этой теме, см. Kotlin Bootcamp for Programmers .
IntelliJ ИДЕЯ
Документацию по IntelliJ IDEA можно найти на веб-сайте JetBrains.
В этом разделе перечислены возможные домашние задания для студентов, которые работают с этой кодовой лабораторией в рамках курса, проводимого инструктором. Инструктор должен сделать следующее:
- При необходимости задайте домашнее задание.
- Объясните учащимся, как сдавать домашние задания.
- Оценивайте домашние задания.
Преподаватели могут использовать эти предложения так мало или так часто, как они хотят, и должны свободно давать любые другие домашние задания, которые они считают подходящими.
Если вы работаете с этой кодовой лабораторией самостоятельно, не стесняйтесь использовать эти домашние задания, чтобы проверить свои знания.
Ответьте на эти вопросы
Вопрос 1
Что из следующего является соглашением по именованию универсального типа?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
вопрос 2
Ограничение на типы, разрешенные для универсального типа, называется:
▢ общее ограничение
▢ общее ограничение
▢ устранение неоднозначности
▢ ограничение общего типа
Вопрос 3
Овеществление означает:
▢ Рассчитано реальное влияние объекта на исполнение.
▢ Для класса установлен ограниченный входной индекс.
▢ Параметр универсального типа преобразован в реальный тип.
▢ Сработал удаленный индикатор ошибки.
Перейти к следующему уроку:
Обзор курса, включая ссылки на другие лаборатории кода, см. в разделе «Kotlin Bootcamp for Programmers: Добро пожаловать на курс».