Эта практическая работа является частью курса Kotlin Bootcamp for Programmers . Вы получите максимальную пользу от этого курса, если будете выполнять задания последовательно. В зависимости от вашего уровня знаний, вы можете пропустить некоторые разделы. Этот курс ориентирован на программистов, владеющих объектно-ориентированным языком программирования и желающих изучить Kotlin .
Введение
В этой лабораторной работе вы познакомитесь с рядом различных полезных функций Kotlin, включая пары, коллекции и функции расширения.
Вместо создания одного примера приложения уроки этого курса направлены на углубление ваших знаний, но при этом они частично независимы друг от друга, чтобы вы могли бегло просмотреть знакомые разделы. Чтобы связать их воедино, многие примеры используют тему аквариума. А если вы хотите узнать всё об аквариуме, ознакомьтесь с курсом Kotlin Bootcamp for Programmers на Udacity.
Что вам уже следует знать
- Синтаксис функций, классов и методов Kotlin
- Как работать с REPL (цикл чтения-вычисления-печати) Kotlin в IntelliJ IDEA
- Как создать новый класс в IntelliJ IDEA и запустить программу
Чему вы научитесь
- Как работать с парами и тройками
- Подробнее о коллекциях
- Определение и использование констант
- Написание функций расширения
Что ты будешь делать?
- Узнайте о парах, тройках и хэш-картах в REPL
- Изучите различные способы организации констант
- Напишите функцию расширения и свойство расширения
В этом задании вы изучите пары и тройки, а также их деструктуризацию. Пары и тройки — это готовые классы данных для двух или трёх общих элементов. Это может быть полезно, например, для того, чтобы функция возвращала более одного значения.
Предположим, у вас есть List
рыб и функция isFreshWater()
для проверки принадлежности рыбы к пресноводной или морской воде. List.partition()
возвращает два списка: один с элементами, для которых условие true
, а другой — с элементами, для которых условие false
.
val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")
Шаг 1: Создайте несколько пар и троек
- Откройте REPL ( Инструменты > Kotlin > Kotlin REPL ).
- Создайте пару, связав единицу оборудования с её назначением, а затем выведите значения. Пару можно создать, создав выражение, связывающее два значения, например, две строки, с ключевым словом
to
, а затем используя.first
или.second
для ссылки на каждое значение.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
- Создайте тройку и выведите её на экран с помощью
toString()
, а затем преобразуйте её в список с помощьюtoList()
. Тройка создаётся с помощьюTriple()
с тремя значениями. Для ссылки на каждое значение используйте.first
,.second
и.third
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42) [6, 9, 42]
В приведённых выше примерах для всех частей пары или тройки используется один и тот же тип, но это не обязательно. Части могут быть строкой, числом, списком или даже другой парой или тройкой.
- Создайте пару, где первая часть пары сама является парой.
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 присвоит значение каждой части по порядку.
- Деструктурируйте пару и выведите значения.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
- Деструктурируйте тройку и выведите значения.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42
Обратите внимание, что деструктуризация пар и троек работает так же, как и с классами данных, что было рассмотрено в предыдущей лабораторной работе.
В этом задании вы узнаете больше о коллекциях, включая списки, и о новом типе коллекций — хэш-картах.
Шаг 1: Узнайте больше о списках
- Списки и изменяемые списки были представлены в предыдущем уроке. Это очень полезная структура данных, поэтому в Kotlin есть ряд встроенных функций для работы со списками. Ознакомьтесь с этим частичным списком функций для списков. Полный список функций можно найти в документации Kotlin по
List
иMutableList
.
Функция | Цель |
| Добавить элемент в изменяемый список. |
| Удалить элемент из изменяемого списка. |
| Верните копию списка с элементами в обратном порядке. |
| Возвращает |
| Возвращает часть списка, начиная с первого индекса и заканчивая вторым индексом, но не включая его. |
- Продолжая работать в REPL, создайте список чисел и вызовите для него функцию
sum()
. Это суммирует все элементы.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
- Создайте список строк и просуммируйте список.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
- Если элемент не является элементом, который
List
умеет суммировать напрямую, например, строкой, вы можете указать способ его суммирования, используя.sumBy()
с лямбда-функцией, например, для суммирования по длине каждой строки. Имя лямбда-аргумента по умолчанию —it
, и здесьit
ссылается на каждый элемент списка по мере его обхода.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
- Списки позволяют делать гораздо больше. Один из способов ознакомиться с доступными функциями — создать список в IntelliJ IDEA, добавить точку и посмотреть на список автодополнения во всплывающей подсказке. Это работает для любого объекта. Попробуйте на списке.
- Выберите
listIterator()
из списка, затем пройдитесь по списку с помощью оператораfor
и выведите все элементы, разделенные пробелами.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
println("$s ")
}
⇒ a bbb cc
Шаг 2: Попробуйте хэш-карты
В Kotlin можно сопоставить практически что угодно с чем угодно, используя hashMapOf()
. Хэш-карты — это своего рода список пар, где первое значение выступает в качестве ключа.
- Создайте хеш-карту, которая сопоставляет симптомы, ключи и болезни рыб, значения.
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
- Затем вы можете получить значение заболевания на основе ключа симптома, используя
get()
или даже более короткие квадратные скобки[]
.
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
- Попробуйте указать симптом, которого нет на карте.
println(cures["scale loss"])
⇒ null
Если ключ отсутствует в карте, попытка вернуть соответствующее заболевание вернёт null
. В зависимости от данных карты, совпадений для возможного ключа может не быть. Для таких случаев в Kotlin предусмотрена функция getOrDefault()
.
- Попробуйте найти ключ, которому нет совпадений, используя
getOrDefault()
.
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know
Если вам нужно сделать больше, чем просто вернуть значение, Kotlin предоставляет функцию getOrElse()
.
- Измените свой код так, чтобы он использовал
getOrElse()
вместоgetOrDefault()
.
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this
Вместо возврата простого значения по умолчанию выполняется код, заключенный в фигурные скобки {}
. В этом примере else
просто возвращает строку, но это может быть и более сложным, например, поиск веб-страницы с лекарством и её возврат.
Как и mutableListOf
, вы также можете создать mutableMapOf
. Изменяемая карта позволяет добавлять и удалять элементы. «Изменяемая» означает возможность изменения, а «неизменяемая» — невозможность изменения.
- Создайте карту инвентаря, которую можно изменять, сопоставляя строку оборудования с количеством предметов. Создайте её, добавив в неё рыболовную сеть, затем добавьте в инвентарь 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
- Попробуйте создать числовую константу в 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
. Сопутствующий объект, по сути, представляет собой одиночный объект внутри класса.
- Создайте класс с сопутствующим объектом, содержащим строковую константу.
class MyClass {
companion object {
const val CONSTANT3 = "constant in companion"
}
}
Основное различие между объектами-компаньонами и обычными объектами заключается в следующем:
- Сопутствующие объекты инициализируются из статического конструктора содержащего класса, то есть они создаются при создании объекта.
- Обычные объекты инициализируются лениво при первом доступе к этому объекту, то есть при их первом использовании.
Это еще не все, но все, что вам нужно знать на данный момент, — это обертывание констант в классах в сопутствующий объект.
В этом задании вы узнаете о расширении поведения классов. Для расширения поведения класса часто пишутся вспомогательные функции. Kotlin предоставляет удобный синтаксис для объявления таких вспомогательных функций: функции расширения.
Функции расширения позволяют добавлять функции в существующий класс без доступа к его исходному коду. Например, вы можете объявить их в файле Extensions.kt , входящем в ваш пакет. Это фактически не изменяет класс, но позволяет использовать точечную нотацию при вызове функции для объектов этого класса.
Шаг 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
- Функцию
hasSpaces()
можно упростить. Ключевое словоthis
не требуется явно, и функцию можно свести к одному выражению и вернуть, поэтому фигурные скобки{}
вокруг неё тоже не нужны.
fun String.hasSpaces() = find { it == ' ' } != null
Шаг 2: Изучите ограничения расширений
Функции расширения имеют доступ только к публичному API класса, который они расширяют. Доступ к private
переменным невозможен.
- Попробуйте добавить функции расширения к свойству, помеченному как
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'
- Изучите код ниже и выясните, что он напечатает.
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()
также выведет GreenLeafyPlant
, поскольку ему было присвоено значение plant
. Но тип определяется во время компиляции, поэтому выводится AquariumPlant
.
Шаг 3: Добавьте свойство расширения
Помимо функций расширения, Kotlin также позволяет добавлять свойства расширения. Как и в случае с функциями расширения, вы указываете класс, который расширяете, затем точку и имя свойства.
- Продолжаем работать в REPL, добавляем свойство расширения
isGreen
кAquariumPlant
, которое принимает значениеtrue
, если цвет зеленый.
val AquariumPlant.isGreen: Boolean
get() = color == "green"
К свойству isGreen
можно получить доступ так же, как к обычному свойству; при обращении вызывается геттер для isGreen
, чтобы получить значение.
- Выведите свойство
isGreen
для переменнойaquariumPlant
и наблюдайте за результатом.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true
Шаг 4: Узнайте о приемниках, допускающих значение NULL
Класс, который вы расширяете, называется приёмником (receiver) , и его можно сделать допускающим значение NULL. В этом случае переменная this
, используемая в теле, может быть null
, поэтому обязательно проверьте это. Вам следует использовать приёмник, допускающий значение NULL, если вы ожидаете, что вызывающие функции будут вызывать ваш метод расширения для переменных, допускающих значение NULL, или если вы хотите обеспечить поведение по умолчанию при применении функции к null
.
- Продолжая работу в REPL, определите метод
pull()
, принимающий приемник, допускающий значение NULL. Это обозначается вопросительным знаком?
после типа перед точкой. Внутри тела можно проверить, не равен лиthis
null
с помощью questionmark-dot-apply?.apply.
fun AquariumPlant?.pull() {
this?.apply {
println("removing $this")
}
}
val plant: AquariumPlant? = null
plant.pull()
- В этом случае при запуске программы вывод данных отсутствует. Поскольку
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") }
Документация Kotlin
Если вам нужна дополнительная информация по какой-либо теме этого курса или вы застряли, лучшей отправной точкой будет https://kotlinlang.org .
-
Pair
-
Triple
-
List
-
MutableList
-
HashMap
- Сопутствующие объекты
- Расширения
- Приемник, допускающий значение NULL
Учебники по Kotlin
На сайте https://try.kotlinlang.org вы найдете подробные учебные пособия по Kotlin Koans, веб-интерпретатор и полный набор справочной документации с примерами.
Курс Udacity
Чтобы просмотреть курс Udacity по этой теме, см. Kotlin Bootcamp for Programmers .
IntelliJ IDEA
Документацию по 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
?
▢ на верхнем уровне файла
▢ в обычных классах
▢ в одиночных объектах
▢ в сопутствующих объектах
Перейти к следующему уроку:
Обзор курса, включая ссылки на другие практические занятия, см. в статье «Kotlin Bootcamp for Programmers: Welcome to the course».