Kotlin Bootcamp for Programmers 5.1: Расширения

Эта лабораторная работа является частью курса Kotlin Bootcamp for Programmers . Вы получите максимальную отдачу от этого курса, если будете последовательно работать с лабораториями кода. В зависимости от ваших знаний, вы можете просмотреть некоторые разделы. Этот курс ориентирован на программистов, которые знают объектно-ориентированный язык и хотят изучить Kotlin .

Введение

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

Вместо того, чтобы создавать один пример приложения, уроки этого курса предназначены для расширения ваших знаний, но они частично независимы друг от друга, поэтому вы можете просматривать разделы, с которыми вы знакомы. Чтобы связать их вместе, во многих примерах используется тема аквариума. А если вы хотите увидеть полную историю аквариума, ознакомьтесь с курсом Kotlin Bootcamp for Programmers Udacity.

Что вы уже должны знать

  • Синтаксис функций, классов и методов Kotlin
  • Как работать с Kotlin REPL (цикл чтения-оценки-печати) в IntelliJ IDEA
  • Как создать новый класс в IntelliJ IDEA и запустить программу

Что вы узнаете

  • Как работать с парами и тройками
  • Подробнее о коллекциях
  • Определение и использование констант
  • Написание функций расширения

Что ты будешь делать

  • Узнайте о парах, тройках и хэш-картах в REPL
  • Изучите различные способы организации констант
  • Напишите функцию расширения и свойство расширения

В этом задании вы узнаете о парах и тройках и их деструктурировании. Пары и тройки — это готовые классы данных для 2 или 3 общих элементов. Например, это может быть полезно, если функция возвращает более одного значения.

Предположим, у вас есть List рыб и функция isFreshWater() для проверки, была ли рыба пресноводной или морской. List.partition() возвращает два списка: один с элементами, для которых условие true , а другой — для элементов, для которых условие false .

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

Шаг 1: Сделайте несколько пар и троек

  1. Откройте REPL ( Инструменты > Kotlin > Kotlin REPL ).
  2. Создайте пару, связав часть оборудования с тем, для чего она используется, затем распечатайте значения. Вы можете создать пару, создав выражение, соединяющее два значения, например две строки, с ключевым словом to , а затем используя .first или .second для ссылки на каждое значение.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. Создайте тройку и распечатайте ее с помощью toString() , а затем преобразуйте в список с помощью toList() . Вы создаете тройку, используя Triple() с 3 значениями. Используйте .first , .second и .third для ссылки на каждое значение.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

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

  1. Создайте пару, где первая часть пары сама является парой.
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment
⇒ catching fish

Шаг 2: Разрушьте некоторые пары и тройки

Разделение пар и троек на их части называется деструктурированием . Назначьте пару или тройку соответствующему количеству переменных, и Kotlin присвоит значение каждой части по порядку.

  1. Деструктурируйте пару и распечатайте значения.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. Деструктурируйте тройку и выведите значения.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

Обратите внимание, что деструктурирование пар и троек работает так же, как и с классами данных, которые рассматривались в предыдущей лаборатории кода.

В этом задании вы узнаете больше о коллекциях, включая списки, и о новом типе коллекции — хэш-картах.

Шаг 1. Подробнее о списках

  1. Списки и изменяемые списки были представлены в предыдущем уроке. Это очень полезная структура данных, поэтому Kotlin предоставляет ряд встроенных функций для списков. Просмотрите этот неполный список функций для списков. Вы можете найти полные списки в документации Kotlin для List и MutableList .

Функция

Цель

add(element: E)

Добавьте элемент в изменяемый список.

remove(element: E)

Удалить элемент из изменяемого списка.

reversed()

Возвращает копию списка с элементами в обратном порядке.

contains(element: E)

Возвращает true , если список содержит элемент.

subList(fromIndex: Int, toIndex: Int)

Возвращает часть списка, от первого индекса до второго индекса, но не включая его.

  1. Продолжая работать в REPL, создайте список чисел и вызовите для него sum() . Это суммирует все элементы.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Создайте список строк и суммируйте список.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. Если элемент не является чем-то, что List умеет суммировать напрямую, например строкой, вы можете указать, как его суммировать, используя .sumBy() с лямбда-функцией, например, для суммирования по длине каждой строки. Имя по умолчанию для лямбда-аргумента — it и здесь it относится к каждому элементу списка по мере обхода списка.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. Со списками можно делать гораздо больше. Один из способов увидеть доступные функции — создать список в IntelliJ IDEA, добавить точку, а затем просмотреть список автозавершения во всплывающей подсказке. Это работает для любого объекта. Попробуйте со списком.

  1. Выберите listIterator() из списка, затем пройдитесь по списку с оператором for и напечатайте все элементы, разделенные пробелами.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

Шаг 2. Попробуйте хеш-карты

В Kotlin вы можете сопоставить практически что угодно с чем угодно, используя hashMapOf() . Хэш-карты похожи на список пар, где первое значение действует как ключ.

  1. Создайте хэш-карту, которая соответствует симптомам, ключам и заболеваниям рыб, значениям.
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. Затем вы можете получить значение болезни на основе ключа симптома, используя get() или даже короче, квадратные скобки [] .
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. Попробуйте указать симптом, которого нет на карте.
println(cures["scale loss"])
⇒ null

Если ключа нет на карте, попытка вернуть соответствующее заболевание возвращает null . В зависимости от картографических данных может оказаться, что для возможного ключа нет совпадения. Для таких случаев Kotlin предоставляет getOrDefault() .

  1. Попробуйте найти несоответствующий ключ, используя getOrDefault() .
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

Если вам нужно сделать больше, чем просто вернуть значение, Kotlin предоставляет getOrElse() .

  1. Измените свой код, чтобы использовать getOrElse() вместо getOrDefault() .
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

Вместо того, чтобы возвращать простое значение по умолчанию, выполняется любой код, заключенный в фигурные скобки {} . В этом примере else просто возвращает строку, но это может быть так же интересно, как найти веб-страницу с лекарством и вернуть ее.

Как и mutableListOf , вы также можете создать mutableMapOf . Изменяемая карта позволяет размещать и удалять элементы. Изменчивый просто означает, что он может измениться, неизменный означает, что он не может измениться.

  1. Создайте карту инвентаря, которую можно изменить, сопоставив строку оборудования с количеством предметов. Создайте его с рыболовной сетью, затем добавьте 3 скруббера в инвентарь с помощью put() и удалите рыболовную сеть с помощью remove() .
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}

В этом задании вы узнаете о константах в Kotlin и различных способах их организации.

Шаг 1. Узнайте о const и val

  1. В REPL попробуйте создать числовую константу. В Kotlin вы можете создавать константы верхнего уровня и присваивать им значение во время компиляции, используя const val .
const val rocks = 3

Значение присваивается и не может быть изменено, что очень похоже на объявление обычного val . Так в чем же разница между const val и val ? Значение для const val определяется во время компиляции, тогда как значение для val определяется во время выполнения программы, что означает, что val может быть присвоено функцией во время выполнения.

Это означает, что val может быть присвоено значение из функции, а const val — нет.

val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok

Кроме того, const val работает только на верхнем уровне и в одноэлементных классах, объявленных с помощью object , а не в обычных классах. Вы можете использовать это для создания файла или одноэлементного объекта, содержащего только константы, и импортировать их по мере необходимости.

object Constants {
    const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2

Шаг 2. Создайте объект-компаньон

В Kotlin нет понятия констант уровня класса.

Чтобы определить константы внутри класса, вы должны обернуть их в объекты-компаньоны, объявленные с ключевым словом companion . Объект-компаньон — это, по сути, одноэлементный объект внутри класса.

  1. Создайте класс с сопутствующим объектом, содержащим строковую константу.
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

Основное различие между сопутствующими объектами и обычными объектами заключается в следующем:

  • Объекты-компаньоны инициализируются из статического конструктора содержащего класса, то есть они создаются при создании объекта.
  • Обычные объекты лениво инициализируются при первом доступе к этому объекту; то есть, когда они впервые используются.

Есть еще кое-что, но все, что вам нужно знать на данный момент, — это заключать константы в классах в объект-компаньон.

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

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

Шаг 1: Напишите функцию расширения

  1. Продолжая работать в REPL, напишите простую функцию расширения hasSpaces() , чтобы проверять, содержит ли строка пробелы. Имя функции имеет префикс класса, с которым она работает. Внутри функции this относится к объекту, для которого it вызывается, и к итератору в вызове find() .
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. Вы можете упростить hasSpaces() . this явно не требуется, и функция может быть сведена к одному выражению и возвращена, поэтому фигурные скобки {} вокруг нее также не нужны.
fun String.hasSpaces() = find { it == ' ' } != null

Шаг 2. Узнайте об ограничениях расширений

Функции расширения имеют доступ только к общедоступному API того класса, который они расширяют. Доступ к private переменным невозможен.

  1. Попробуйте добавить функции расширения к свойству с пометкой private .
class AquariumPlant(val color: String, private val size: Int)

fun AquariumPlant.isRed() = color == "red"    // OK
fun AquariumPlant.isBig() = size > 50         // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
  1. Изучите приведенный ниже код и выясните, что он напечатает.
open class AquariumPlant(val color: String, private val size: Int)

class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)

fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")

val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print()  // what will it print?
⇒ GreenLeafyPlant
AquariumPlant

plant.print() печатает GreenLeafyPlant . Вы можете ожидать, что AquariumPlant.print aquariumPlant.print() также напечатает GreenLeafyPlant , потому что ему было присвоено значение plant . Но тип определяется во время компиляции, поэтому AquariumPlant печатается.

Шаг 3. Добавьте свойство расширения

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

  1. Все еще работая в REPL, добавьте свойство расширения isGreen в AquariumPlant , которое true , если цвет зеленый.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

Доступ к свойству isGreen можно получить так же, как к обычному свойству; при доступе вызывается геттер для isGreen , чтобы получить значение.

  1. Выведите свойство isGreen для переменной aquariumPlant и посмотрите на результат.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

Шаг 4. Знайте о получателях, допускающих значение NULL

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

  1. Продолжая работать в REPL, определите метод pull() , который принимает приемник, допускающий значение NULL. Обозначается знаком вопроса ? после шрифта перед точкой. Внутри тела вы можете проверить, не является ли this null , используя вопросительный знак-точка-применить ?.apply.
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. В этом случае при запуске программы нет вывода. Поскольку plant равно null , внутренний метод println() не вызывается.

Функции расширения очень мощные, и большая часть стандартной библиотеки Kotlin реализована как функции расширения.

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

  • Пары и тройки могут использоваться для возврата более одного значения из функции. Например:
    val twoLists = fish.partition { isFreshWater(it) }
  • В Kotlin есть много полезных функций для List , таких как reversed() , contains() и subList() .
  • HashMap можно использовать для сопоставления ключей со значениями. Например:
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • Объявляйте константы времени компиляции с помощью ключевого слова const . Вы можете разместить их на верхнем уровне, организовать их в одиночный объект или поместить в сопутствующий объект.
  • Объект-компаньон — это одноэлементный объект в определении класса, определенный с помощью ключевого слова companion .
  • Функции и свойства расширения могут добавлять функциональные возможности к классу. Например:
    fun String.hasSpaces() = find { it == ' ' } != null
  • Приемник, допускающий значение NULL, позволяет создавать расширения класса, которые могут иметь значение null . ?. Оператор может быть связан с оператором apply для проверки на значение null перед выполнением кода. Например:
    this?.apply { println("removing $this") }

Котлин документация

Если вам нужна дополнительная информация по какой-либо теме этого курса или если вы застряли, https://kotlinlang.org — лучшая отправная точка.

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

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

Удасити курс

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

IntelliJ ИДЕЯ

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

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

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

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

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

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

Вопрос 1

Что из следующего возвращает копию списка?

add()

remove()

reversed()

contains()

вопрос 2

Какая из этих функций расширения в class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) выдаст ошибку компилятора?

fun AquariumPlant.isRed() = color == "red"

fun AquariumPlant.isBig() = size > 45

fun AquariumPlant.isExpensive() = cost > 10.00

fun AquariumPlant.isNotLeafy() = leafy == false

Вопрос 3

Что из следующего не является местом, где вы можете определять константы с помощью const val ?

▢ на верхнем уровне файла

▢ в обычных классах

▢ в одноэлементных объектах

▢ в сопутствующих объектах

Перейти к следующему уроку: 5.2 Общие сведения

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