Kotlin Bootcamp for Programmers 5.2: Общие сведения

Эта лабораторная работа является частью курса 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. Создайте иерархию типов

На этом этапе вы создаете несколько классов для использования на следующем этапе. Подклассы были рассмотрены в более ранней кодовой лаборатории, но вот краткий обзор.

  1. Чтобы не загромождать пример, создайте новый пакет в src и назовите его generics .
  2. В пакете generics создайте новый файл Aquarium.kt . Это позволяет вам переопределять вещи, используя одни и те же имена, без конфликтов, поэтому остальная часть вашего кода для этой лаборатории кода помещается в этот файл.
  3. Составьте типовую иерархию типов водоснабжения. Начните с того, что WaterSupply open классом, чтобы он мог быть подклассом.
  4. Добавьте логический параметр var , needsProcessing . Это автоматически создает изменяемое свойство вместе с геттером и сеттером.
  5. Создайте подкласс TapWater , который расширяет WaterSupply , и передайте true для needsProcessing , потому что вода из-под крана содержит вредные для рыб добавки.
  6. В TapWater определите функцию addChemicalCleaners() , которая устанавливает для needsProcessing значение false после очистки воды. Свойство needsProcessing можно установить из TapWater , так как оно public по умолчанию и доступно для подклассов. Вот готовый код.
package generics

open class WaterSupply(var needsProcessing: Boolean)

class TapWater : WaterSupply(true) {
   fun addChemicalCleaners() {
       needsProcessing = false
   }
}
  1. Создайте еще два подкласса WaterSupply , FishStoreWater и LakeWater . FishStoreWater не нуждается в обработке, но LakeWater необходимо фильтровать с помощью метода filter() . После фильтрации его не нужно снова обрабатывать, поэтому в filter() установите needsProcessing = false .
class FishStoreWater : WaterSupply(false)

class LakeWater : WaterSupply(true) {
   fun filter() {
       needsProcessing = false
   }
}

Если вам нужна дополнительная информация, просмотрите предыдущий урок о наследовании в Kotlin.

Шаг 2: Создайте общий класс

На этом шаге вы модифицируете класс Aquarium для поддержки различных типов водоснабжения.

  1. В Aquarium.kt определите класс Aquarium , поставив <T> в скобках после имени класса.
  2. Добавьте неизменяемое свойство waterSupply типа T в Aquarium .
class Aquarium<T>(val waterSupply: T)
  1. Напишите функцию с именем genericsExample() . Это не часть класса, поэтому он может находиться на верхнем уровне файла, как функция main() или определения классов. В функции создайте Aquarium и передайте ему WaterSupply . Поскольку параметр waterSupply является универсальным, вы должны указать тип в угловых скобках <> .
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
}
  1. В genericsExample() ваш код может получить доступ к waterSupply аквариума. Поскольку это тип TapWater , вы можете вызывать addChemicalCleaners() без приведения типов.
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. При создании объекта « Aquarium » вы можете удалить угловые скобки и то, что находится между ними, потому что в Kotlin есть вывод типов. Так что нет причин повторять TapWater дважды при создании экземпляра. Тип можно определить по аргументу Aquarium ; он по-прежнему будет создавать Aquarium типа TapWater .
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. Чтобы увидеть, что происходит, напечатайте 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}")
}
  1. Добавьте функцию main() для вызова genericsExample() , затем запустите свою программу и посмотрите на результат.
fun main() {
    genericsExample()
}
⇒ water needs processing: true
water needs processing: false

Шаг 3. Сделайте его более конкретным

Общий означает, что вы можете передать почти что угодно, и иногда это проблема. На этом этапе вы делаете класс Aquarium более конкретным в отношении того, что вы можете в него поместить.

  1. В genericsExample() создайте Aquarium , передав строку для waterSupply , затем напечатайте свойство waterSupply аквариума.
fun genericsExample() {
    val aquarium2 = Aquarium("string")
    println(aquarium2.waterSupply)
}
  1. Запустите вашу программу и наблюдайте за результатом.
⇒ string

Результатом является переданная вами строка, потому что Aquarium не накладывает никаких ограничений на T. Можно передать любой тип, включая String .

  1. В genericsExample() создайте еще один Aquarium , передав null для waterSupply . Если значение waterSupply равно null, выведите "waterSupply is null" .
fun genericsExample() {
    val aquarium3 = Aquarium(null)
    if (aquarium3.waterSupply == null) {
        println("waterSupply is null")
    }
}
  1. Запустите вашу программу и наблюдайте за результатом.
⇒ waterSupply is null

Почему вы можете передать значение null при создании Aquarium ? Это возможно, потому что по умолчанию T означает значение Any? допускающее значение NULL. тип, тип на вершине иерархии типов. Следующее эквивалентно тому, что вы ввели ранее.

class Aquarium<T: Any?>(val waterSupply: T)
  1. Чтобы запретить передачу null , сделайте T типа Any явно, удалив ? после Any .
class Aquarium<T: Any>(val waterSupply: T)

В этом контексте Any называется общим ограничением . Это означает, что любой тип может быть передан для T , если он не равен null .

  1. Что вы действительно хотите, так это убедиться, что только WaterSupply (или один из его подклассов) может быть передан для T . Замените Any на WaterSupply , чтобы определить более конкретное общее ограничение.
class Aquarium<T: WaterSupply>(val waterSupply: T)

Шаг 4. Добавьте больше проверок

На этом шаге вы узнаете о функции check() , которая поможет убедиться, что ваш код ведет себя так, как ожидалось. Функция check() — это стандартная библиотечная функция в Kotlin. Он действует как утверждение и IllegalStateException , если его аргумент оценивается как false .

  1. Добавьте метод 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() выдаст исключение.

  1. В genericsExample() добавьте код для создания Aquarium с помощью LakeWater , а затем добавьте в него немного воды.
fun genericsExample() {
    val aquarium4 = Aquarium(LakeWater())
    aquarium4.addWater()
}
  1. Запустите вашу программу, и вы получите исключение, потому что воду нужно сначала отфильтровать.
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first
        at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)
  1. Добавьте призыв фильтровать воду перед добавлением ее в 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: Определите тип выхода

  1. В классе Aquarium измените T: WaterSupply out выходной тип.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
    ...
}
  1. В том же файле вне класса объявите функцию addItemTo() , которая ожидает Aquarium of WaterSupply .
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
  1. Вызовите addItemTo() из genericsExample() и запустите свою программу.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    addItemTo(aquarium)
}
⇒ item added

Kotlin может гарантировать, что addItemTo() не сделает ничего небезопасного с общим WaterSupply , потому что он объявлен как тип out .

  1. Если вы удалите ключевое слово out , компилятор выдаст ошибку при вызове addItemTo() , потому что Kotlin не может гарантировать, что вы не делаете ничего небезопасного с типом.

Шаг 2: Определите тип ввода

Тип in похож на тип out , но для универсальных типов, которые только когда-либо передаются в функции, а не возвращаются. Если вы попытаетесь вернуть тип in , вы получите ошибку компилятора. В этом примере вы определите тип in как часть интерфейса.

  1. В Aquarium.kt определите интерфейс Cleaner , который принимает общий T , ограниченный WaterSupply . Поскольку он используется только в качестве аргумента для clean() , вы можете сделать его параметром in .
interface Cleaner<in T: WaterSupply> {
    fun clean(waterSupply: T)
}
  1. Чтобы использовать интерфейс Cleaner , создайте класс TapWaterCleaner , который реализует Cleaner для очистки TapWater путем добавления химических веществ.
class TapWaterCleaner : Cleaner<TapWater> {
    override fun clean(waterSupply: TapWater) =   waterSupply.addChemicalCleaners()
}
  1. В классе 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")
    }
}
  1. Обновите код примера genericsExample() , чтобы создать TapWaterCleaner , Aquarium с TapWater , а затем добавьте немного воды с помощью очистителя. Он будет использовать очиститель по мере необходимости.
fun genericsExample() {
    val cleaner = TapWaterCleaner()
    val aquarium = Aquarium(TapWater())
    aquarium.addWater(cleaner)
}

Kotlin будет использовать in о входных и out типах, чтобы убедиться, что ваш код безопасно использует дженерики. Out и in легко запомнить: типы out можно передавать наружу как возвращаемые значения, а типы in можно передавать внутрь как аргументы.

Если вы хотите больше узнать о проблемах в типах и решениях вне типов , документация подробно описывает их.

В этом задании вы узнаете об общих функциях и о том, когда их использовать. Как правило, создание универсальной функции является хорошей идеей, когда функция принимает аргумент класса, имеющего универсальный тип.

Шаг 1: Создайте общую функцию

  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 , сделав функцию универсальной.

  1. Чтобы сделать функцию универсальной, поместите угловые скобки после ключевого слова fun с общим типом T и любыми ограничениями, в данном случае WaterSupply . Измените Aquarium , чтобы он ограничивался T вместо WaterSupply .
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
   println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}

T — это параметр типа для isWaterClean() , который используется для указания общего типа аквариума. Этот паттерн действительно распространен, и было бы неплохо уделить немного времени, чтобы разобраться с ним.

  1. Вызовите isWaterClean() , указав тип в угловых скобках сразу после имени функции и перед круглыми скобками.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    isWaterClean<TapWater>(aquarium)
}
  1. Из-за вывода типа из аргумента aquarium этот тип не нужен, поэтому удалите его. Запустите вашу программу и наблюдайте за выводом.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    isWaterClean(aquarium)
}
⇒ aquarium water is clean: false

Шаг 2: Создайте общий метод с материализованным типом

Вы также можете использовать универсальные функции для методов, даже в классах, которые имеют собственный универсальный тип. На этом шаге вы добавляете общий метод в Aquarium , который проверяет, имеет ли он тип WaterSupply .

  1. В классе Aquarium объявите метод hasWaterSupplyOfType() , который принимает универсальный параметр R ( T уже используется), ограниченный WaterSupply , и возвращает true , если waterSupply имеет тип R Это похоже на функцию, которую вы объявили ранее, но внутри класса Aquarium .
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
  1. Обратите внимание, что последняя R подчеркнута красным. Наведите на него указатель, чтобы увидеть, в чем ошибка.
  2. Чтобы выполнить проверку is вам нужно сообщить Kotlin, что тип reified или реальный и может использоваться в функции. Для этого поместите inline перед ключевым словом fun и reified перед универсальным типом R
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R

Как только тип овеществлен, вы можете использовать его как обычный тип, потому что после встраивания это реальный тип. Это означает, что вы можете выполнять проверки is тип.

Если вы не используете reified здесь, тип не будет достаточно «реальным», чтобы Kotlin разрешил is проверки. Это связано с тем, что неовеществленные типы доступны только во время компиляции и не могут использоваться вашей программой во время выполнения. Это обсуждается более подробно в следующем разделе.

  1. Передайте TapWater в качестве типа. Как и при вызове универсальных функций, вызывайте универсальные методы, используя угловые скобки с типом после имени функции. Запустите вашу программу и наблюдайте за результатом.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.hasWaterSupplyOfType<TapWater>())   // true
}
⇒ true

Шаг 3: Создайте функции расширения

Вы также можете использовать овеществленные типы для обычных функций и функций расширения.

  1. Вне класса Aquarium определите функцию расширения для WaterSupply с именем isOfType() , которая проверяет, относится ли переданный WaterSupply к определенному типу, например, TapWater .
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
  1. Вызовите функцию расширения точно так же, как метод.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.waterSupply.isOfType<TapWater>())  
}
⇒ true

С этими функциями расширения не имеет значения, какой это Aquarium ( Aquarium TowerTank или какой-либо другой подкласс), главное, чтобы это был Aquarium . Использование синтаксиса « звезда-проекция» — удобный способ указать множество совпадений. И когда вы используете звездную проекцию, Kotlin также позаботится о том, чтобы вы не делали ничего небезопасного.

  1. Чтобы использовать звездную проекцию, поставьте <*> после Aquarium . Переместите hasWaterSupplyOfType() в функцию расширения, поскольку на самом деле она не является частью основного API Aquarium .
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
  1. Измените вызов на 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 — лучшая отправная точка.

Учебники по Котлину

На веб-сайте https://try.kotlinlang.org есть подробные руководства под названием Kotlin Koans, веб-интерпретатор и полный набор справочной документации с примерами.

Удасити курс

Чтобы просмотреть курс Udacity по этой теме, см. Kotlin Bootcamp for Programmers .

IntelliJ ИДЕЯ

Документацию по IntelliJ IDEA можно найти на веб-сайте JetBrains.

В этом разделе перечислены возможные домашние задания для студентов, которые работают с этой кодовой лабораторией в рамках курса, проводимого инструктором. Инструктор должен сделать следующее:

  • При необходимости задайте домашнее задание.
  • Объясните учащимся, как сдавать домашние задания.
  • Оценивайте домашние задания.

Преподаватели могут использовать эти предложения так мало или так часто, как они хотят, и должны свободно давать любые другие домашние задания, которые они считают подходящими.

Если вы работаете с этой кодовой лабораторией самостоятельно, не стесняйтесь использовать эти домашние задания, чтобы проверить свои знания.

Ответьте на эти вопросы

Вопрос 1

Что из следующего является соглашением по именованию универсального типа?

<Gen>

<Generic>

<T>

<X>

вопрос 2

Ограничение на типы, разрешенные для универсального типа, называется:

▢ общее ограничение

▢ общее ограничение

▢ устранение неоднозначности

▢ ограничение общего типа

Вопрос 3

Овеществление означает:

▢ Рассчитано реальное влияние объекта на исполнение.

▢ Для класса установлен ограниченный входной индекс.

▢ Параметр универсального типа преобразован в реальный тип.

▢ Сработал удаленный индикатор ошибки.

Перейти к следующему уроку: 6. Функциональная манипуляция

Обзор курса, включая ссылки на другие лаборатории кода, см. в разделе «Kotlin Bootcamp for Programmers: Добро пожаловать на курс».