Эта практическая работа является частью курса Kotlin Bootcamp for Programmers . Вы получите максимальную пользу от этого курса, если будете выполнять задания последовательно. В зависимости от вашего уровня знаний, вы можете пропустить некоторые разделы. Этот курс ориентирован на программистов, владеющих объектно-ориентированным языком программирования и желающих изучить Kotlin .
Введение
Это заключительная практическая работа в рамках Kotlin Bootcamp. В этой практической работе вы узнаете об аннотациях и помеченных разрывах. Вы рассмотрите лямбда-выражения и функции высшего порядка, которые являются ключевыми элементами Kotlin. Вы также узнаете больше о встраивании функций и интерфейсах Single Abstract Method (SAM). Наконец, вы узнаете больше о стандартной библиотеке Kotlin .
Вместо создания одного примера приложения уроки этого курса направлены на углубление ваших знаний, но при этом они частично независимы друг от друга, чтобы вы могли бегло просмотреть знакомые разделы. Чтобы связать их воедино, многие примеры используют тему аквариума. А если вы хотите узнать всё об аквариуме, ознакомьтесь с курсом Kotlin Bootcamp for Programmers на Udacity.
Что вам уже следует знать
- Синтаксис функций, классов и методов Kotlin
- Как создать новый класс в IntelliJ IDEA и запустить программу
- Основы лямбда-выражений и функций высшего порядка
Чему вы научитесь
- Основы аннотаций
- Как использовать маркированные разрывы
- Подробнее о функциях высшего порядка
- Об интерфейсах Single Abstract Method (SAM)
- О стандартной библиотеке Kotlin
Что ты будешь делать?
- Создайте простую аннотацию.
- Используйте обозначенный перерыв.
- Ознакомьтесь с лямбда-функциями в Kotlin.
- Использовать и создавать функции высшего порядка.
- Вызовите некоторые интерфейсы Single Abstract Method.
- Используйте некоторые функции из стандартной библиотеки Kotlin.
Аннотации — это способ добавления метаданных к коду, и они не являются чем-то специфичным только для Kotlin. Аннотации считываются компилятором и используются для генерации кода или логики. Многие фреймворки, такие как Ktor и Kotlinx , а также Room , используют аннотации для настройки своего выполнения и взаимодействия с кодом. Вы вряд ли столкнётесь с аннотациями, пока не начнёте использовать фреймворки, но полезно уметь читать аннотации.
В стандартной библиотеке Kotlin также доступны аннотации, управляющие компиляцией кода. Они очень полезны при экспорте кода Kotlin в Java, но в остальных случаях они не так уж часто нужны.
Аннотации располагаются непосредственно перед аннотируемым объектом, и аннотировать можно большинство объектов: классы, функции, методы и даже управляющие структуры. Некоторые аннотации могут принимать аргументы.
Вот пример некоторых аннотаций.
@file:JvmName("InteropFish")
class InteropFish {
companion object {
@JvmStatic fun interop()
}
} Здесь указано, что экспортированное имя этого файла — InteropFish с аннотацией JvmName ; аннотация JvmName принимает аргумент "InteropFish" . В сопутствующем объекте @JvmStatic указывает Kotlin сделать interop() статической функцией в InteropFish .
Вы также можете создавать свои собственные аннотации, но это в основном полезно, если вы пишете библиотеку, которой нужна конкретная информация о классах во время выполнения, то есть рефлексия .
Шаг 1: Создайте новый пакет и файл
- В src создайте новый пакет,
example. - В примере создайте новый файл Kotlin,
Annotations.kt.
Шаг 2: Создайте свою собственную аннотацию
- В
Annotations.ktсоздайте классPlantс двумя методами:trim()иfertilize().
class Plant {
fun trim(){}
fun fertilize(){}
}- Создайте функцию, которая выводит все методы класса. Используйте
::classдля получения информации о классе во время выполнения. ИспользуйтеdeclaredMemberFunctionsдля получения списка методов класса. (Чтобы получить к нему доступ, необходимо импортироватьkotlin.reflect.full.*)
import kotlin.reflect.full.* // required import
class Plant {
fun trim(){}
fun fertilize(){}
}
fun testAnnotations() {
val classObj = Plant::class
for (m in classObj.declaredMemberFunctions) {
println(m.name)
}
}- Создайте функцию
main()для вызова вашей тестовой процедуры. Запустите программу и посмотрите на результат.
fun main() {
testAnnotations()
}⇒ trim fertilize
- Создайте простую аннотацию
ImAPlant.
annotation class ImAPlantЭто не делает ничего, кроме того, что указывает на наличие аннотации.
- Добавьте аннотацию перед классом
Plant.
@ImAPlant class Plant{
...
}- Измените функцию
testAnnotations(), чтобы вывести все аннотации класса. Используйте функциюannotationsдля получения всех аннотаций класса. Запустите программу и посмотрите на результат.
fun testAnnotations() {
val plantObject = Plant::class
for (a in plantObject.annotations) {
println(a.annotationClass.simpleName)
}
}⇒ ImAPlant
- Измените
testAnnotations(), чтобы найти аннотациюImAPlant. ИспользуйтеfindAnnotation()чтобы найти конкретную аннотацию. Запустите программу и посмотрите на результат.
fun testAnnotations() {
val plantObject = Plant::class
val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
println(myAnnotationObject)
}
⇒ @example.ImAPlant()
Шаг 3: Создайте целевую аннотацию
Аннотации могут быть нацелены на геттеры или сеттеры. В этом случае их можно применять с префиксом @get: или @set: :. Это часто встречается при использовании фреймворков с аннотациями.
- Объявите две аннотации:
OnGet, которую можно применять только к геттерам свойств, иOnSet, которую можно применять только к сеттерам свойств. Используйте@Target(AnnotationTarger.PROPERTY_GETTER)илиPROPERTY_SETTERдля каждой из них.
annotation class ImAPlant
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class OnGet
@Target(AnnotationTarget.PROPERTY_SETTER)
annotation class OnSet
@ImAPlant class Plant {
@get:OnGet
val isGrowing: Boolean = true
@set:OnSet
var needsFood: Boolean = false
}Аннотации — действительно мощный инструмент для создания библиотек, которые проверяют код как во время выполнения, так и иногда во время компиляции. Однако типичный код приложения использует только аннотации, предоставляемые фреймворками.
В Kotlin есть несколько способов управления потоком выполнения. Вы уже знакомы с return , который возвращает выполнение из функции в содержащую её функцию. Использование оператора break аналогично return , но для циклов.
Kotlin предоставляет дополнительный контроль над циклами с помощью так называемого помеченного прерывания (break) . break , определённое меткой, переходит к точке выполнения сразу после цикла, помеченного этой меткой. Это особенно полезно при работе с вложенными циклами.
Любое выражение в Kotlin можно пометить меткой. Метки представляют собой идентификатор, за которым следует символ @ .
- В
Annotations.ktпопробуйте помеченный break, выйдя из внутреннего цикла.
fun labels() {
outerLoop@ for (i in 1..100) {
print("$i ")
for (j in 1..100) {
if (i > 10) break@outerLoop // breaks to outer loop
}
}
}
fun main() {
labels()
}- Запустите программу и посмотрите на результат.
⇒ 1 2 3 4 5 6 7 8 9 10 11
Аналогично, можно использовать помеченный continue . Вместо того, чтобы выйти из помеченного цикла, помеченный continue переходит к следующей итерации цикла.
Лямбда-выражения — это анонимные функции, то есть функции без имени. Вы можете присваивать их переменным и передавать в качестве аргументов функциям и методам. Они чрезвычайно полезны.
Шаг 1: Создайте простую лямбда-функцию
- Запустите REPL в IntelliJ IDEA, Инструменты > Kotlin > Kotlin REPL .
- Создайте лямбда-выражение с аргументом
dirty: Int, которое выполняет вычисление, деляdirtyна 2. Присвойте лямбда-выражение переменнойwaterFilter.
val waterFilter = { dirty: Int -> dirty / 2 }- Вызовите
waterFilter, передав значение 30.
waterFilter(30)⇒ res0: kotlin.Int = 15
Шаг 2: Создание лямбда-фильтра
- Оставаясь в REPL, создайте класс данных
Fishс одним свойствомname.
data class Fish(val name: String)- Составьте список из 3
Fishс именами Флиппер, Моби Дик и Дори.
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))- Добавьте фильтр для проверки имен, содержащих букву «i».
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]
В лямбда-выражении it относится к текущему элементу списка, а фильтр применяется к каждому элементу списка по очереди.
- Примените
joinString()к результату, используя", "в качестве разделителя.
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick
Функция joinToString() создаёт строку, объединяя отфильтрованные имена, разделённые заданной строкой. Это одна из многих полезных функций, встроенных в стандартную библиотеку Kotlin.
Передача лямбда-функции или другой функции в качестве аргумента создаёт функцию высшего порядка. Фильтр выше — простой пример. filter() — это функция, которой передаётся лямбда-функция, определяющая способ обработки каждого элемента списка.
Написание функций высшего порядка с использованием лямбда-выражений расширения — одна из самых продвинутых частей языка Kotlin. Научиться писать их довольно сложно, но использовать их очень удобно.
Шаг 1: Создайте новый класс
- В пакете примеров создайте новый файл Kotlin,
Fish.kt - В
Fish.ktсоздайте класс данныхFishс одним свойствомname.
data class Fish (var name: String)- Создайте функцию
fishExamples(). ВfishExamples()создайте рыбу с именем"splashy", все буквы — строчные.
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
}- Создайте функцию
main(), которая вызываетfishExamples().
fun main () {
fishExamples()
}- Скомпилируйте и запустите программу, щёлкнув по зелёному треугольнику слева от
main(). Результат пока отсутствует.
Шаг 2: Используйте функцию высшего порядка
Функция with() позволяет создать одну или несколько ссылок на объект или свойство более компактным способом. Используя this , with() фактически является функцией высшего порядка, и в лямбда-функции вы указываете, что делать с предоставленным объектом.
- Используйте
with()для написания названия рыбы с заглавной буквы вfishExamples(). Внутри фигурных скобокthisотносится к объекту, переданному вwith().
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
with (fish.name) {
this.capitalize()
}
}- Вывода нет, поэтому добавьте
println()вокруг него. Аthisнеявный и не нужен, поэтому его можно удалить.
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
with (fish.name) {
println(capitalize())
}
}⇒ Splashy
Шаг 3: Создайте функцию высшего порядка
По сути, with() — это функция высшего порядка. Чтобы увидеть, как это работает, вы можете создать собственную, значительно упрощённую версию with() , которая работает только со строками.
- В
Fish.ktопределите функциюmyWith(), принимающую два аргумента. Аргументами являются объект, над которым выполняется операция, и функция, определяющая операцию. Имя аргумента для функции принято обозначать какblock. В данном случае функция ничего не возвращает, что определяется с помощьюUnit.
fun myWith(name: String, block: String.() -> Unit) {}Внутри myWith() функция block() теперь является функцией расширения String . Расширяемый класс часто называется объектом-приёмником . Поэтому в данном случае name — это объект-приёмник.
- В теле
myWith()примените переданную функциюblock()к объекту-получателюname.
fun myWith(name: String, block: String.() -> Unit) {
name.block()
}- В
fishExamples()заменитеwith()наmyWith().
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
myWith (fish.name) {
println(capitalize())
}
}fish.name — аргумент имени, а println(capitalize()) — блочная функция.
- Запустите программу, и она будет работать как и прежде.
⇒ Splashy
Шаг 4: Изучите другие встроенные расширения
Расширение with() очень полезно и входит в стандартную библиотеку Kotlin . Вот несколько других, которые могут вам пригодиться: run() , apply() и let() .
Функция run() — это расширение, работающее со всеми типами. Она принимает в качестве аргумента одно лямбда-выражение и возвращает результат его выполнения.
- В
fishExamples()вызовитеrun()дляfish, чтобы получить имя.
fish.run {
name
}Этот метод просто возвращает свойство name . Вы можете присвоить его переменной или вывести на экран. На самом деле, это не очень полезный пример, поскольку можно просто получить доступ к свойству, но run() может быть полезен для более сложных выражений.
Функция apply() похожа на run() , но возвращает изменённый объект, к которому она была применена, а не результат лямбда-выражения. Это может быть полезно для вызова методов для вновь созданного объекта.
- Создайте копию
fishи вызовитеapply()чтобы задать имя новой копии.
val fish2 = Fish(name = "splashy").apply {
name = "sharky"
}
println(fish2.name)
⇒ sharky
Функция let() похожа на apply() , но возвращает копию объекта с изменениями. Это может быть полезно для объединения манипуляций в цепочку.
- Используйте
let(), чтобы получить названиеfish, напишите его заглавными буквами, присоедините к нему другую строку, получите длину полученного результата, прибавьте к длине 31, а затем выведите результат.
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})⇒ 42
В этом примере тип объекта, на который it ссылается, — Fish , затем String , затем снова String и, наконец, Int .
- Выведите
fishпосле вызоваlet(), и вы увидите, что он не изменился.
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
println(fish)⇒ 42 Fish(name=splashy)
Лямбда-выражения и функции высшего порядка действительно полезны, но вам следует знать кое-что: лямбда-выражения — это объекты. Лямбда-выражение — это экземпляр интерфейса Function , который, в свою очередь, является подтипом Object . Рассмотрим предыдущий пример myWith() .
myWith(fish.name) {
capitalize()
}Интерфейс Function содержит метод invoke() , который переопределяется для вызова лямбда-выражения. В чистом виде это будет выглядеть примерно так, как показано ниже.
// actually creates an object that looks like this
myWith(fish.name, object : Function1<String, Unit> {
override fun invoke(name: String) {
name.capitalize()
}
})Обычно это не проблема, поскольку создание объектов и вызов функций не требуют больших накладных расходов, то есть памяти и процессорного времени. Но если вы определяете что-то вроде myWith() и используете его повсеместно, накладные расходы могут возрасти.
В Kotlin предусмотрена inline , которая позволяет решить эту проблему, снижая накладные расходы во время выполнения, добавляя немного работы компилятору. (Вы немного узнали о inline в предыдущем уроке, когда говорили о конкретизированных типах.) Обозначение функции как inline означает, что при каждом вызове функции компилятор фактически преобразует исходный код во «встроенную» функцию. То есть компилятор изменит код, заменив лямбда-выражение инструкциями внутри лямбда-выражения.
Если myWith() в примере выше помечен как inline :
inline myWith(fish.name) {
capitalize()
}он трансформируется в прямой вызов:
// with myWith() inline, this becomes
fish.name.capitalize()Стоит отметить, что встраивание больших функций увеличивает размер кода, поэтому его лучше всего применять для простых функций, которые используются многократно, например, myWith() . Функции расширения из библиотек, о которых вы узнали ранее, помечены как inline , поэтому вам не нужно беспокоиться о создании дополнительных объектов.
Single Abstract Method означает интерфейс с одним методом. Они очень распространены при использовании API, написанных на языке программирования Java, поэтому для них существует аббревиатура SAM. Примерами служат Runnable , имеющий один абстрактный метод run() , и Callable , имеющий один абстрактный метод call() .
В Kotlin вам постоянно приходится вызывать функции, принимающие SAM в качестве параметров. Попробуйте пример ниже.
- Внутри примера создайте класс Java
JavaRunи вставьте в файл следующее.
package example;
public class JavaRun {
public static void runNow(Runnable runnable) {
runnable.run();
}
}Kotlin позволяет создать экземпляр объекта, реализующего интерфейс, добавив к типу object: . Это полезно для передачи параметров в SAM.
- Вернитесь в
Fish.kt, создайте функциюrunExample(), которая создаетRunnableс помощьюobject:Объект должен реализоватьrun(), выведя"I'm a Runnable".
fun runExample() {
val runnable = object: Runnable {
override fun run() {
println("I'm a Runnable")
}
}
}- Вызовите
JavaRun.runNow()с созданным вами объектом.
fun runExample() {
val runnable = object: Runnable {
override fun run() {
println("I'm a Runnable")
}
}
JavaRun.runNow(runnable)
}- Вызовите
runExample()изmain()и запустите программу.
⇒ I'm a Runnable
Много работы, чтобы что-то вывести, но это хороший пример того, как работает SAM. Конечно, в Kotlin есть более простой способ сделать это — использовать лямбда-выражение вместо объекта, что делает код гораздо компактнее.
- Удалите существующий код в
runExample, измените его так, чтобы он вызывалrunNow()с лямбда-функцией, и запустите программу.
fun runExample() {
JavaRun.runNow({
println("Passing a lambda as a Runnable")
})
}
⇒ Passing a lambda as a Runnable
- Вы можете сделать это еще более кратко, используя синтаксис вызова последнего параметра и избавившись от скобок.
fun runExample() {
JavaRun.runNow {
println("Last parameter is a lambda as a Runnable")
}
}⇒ Last parameter is a lambda as a Runnable
Это основы SAM, единого абстрактного метода. Вы можете создать экземпляр, переопределить и вызвать SAM одной строкой кода, используя шаблон:
Class.singleAbstractMethod { lambda_of_override }
В этом уроке мы рассмотрели лямбда-выражения и более подробно рассмотрели функции высшего порядка — ключевые элементы Kotlin. Вы также узнали об аннотациях и помеченных разрывах.
- Используйте аннотации, чтобы указать компилятору определённые данные. Например:
@file:JvmName("Foo") - Используйте помеченные прерывания, чтобы разрешить выход из вложенных циклов. Например:
if (i > 10) break@outerLoop // breaks to outerLoop label - Лямбда-выражения могут быть очень эффективными в сочетании с функциями более высокого порядка.
- Лямбда-выражения — это объекты. Чтобы избежать создания объекта, можно пометить функцию как
inline, и компилятор добавит содержимое лямбда-выражения непосредственно в код. - Используйте
inlineосторожно, но это может помочь сократить использование ресурсов вашей программой. - SAM (Единый абстрактный метод) — распространённый шаблон, упрощённый с помощью лямбда-выражений. Базовый шаблон выглядит следующим образом:
Class.singleAbstractMethod { lamba_of_override } - Стандартная библиотека Kotlin предоставляет множество полезных функций, включая несколько SAM, поэтому узнайте, что в ней есть.
Kotlin гораздо шире, чем было рассмотрено в курсе, но теперь у вас есть основы, необходимые для разработки собственных программ на Kotlin. Надеюсь, вам понравится этот выразительный язык и вы с нетерпением ждёте возможности создавать больше функциональности, используя меньше кода (особенно если вы работаете с языком программирования Java). Практика и постоянное обучение — лучший способ стать экспертом в Kotlin, поэтому продолжайте изучать Kotlin самостоятельно.
Документация Kotlin
Если вам нужна дополнительная информация по какой-либо теме этого курса или вы застряли, лучшей отправной точкой будет https://kotlinlang.org .
- Аннотации
- Отражение
- Маркированные перерывы
- Функции высшего порядка и лямбда-выражения
- Встроенные функции
Учебники по Kotlin
На сайте https://try.kotlinlang.org вы найдете подробные учебные пособия по Kotlin Koans, веб-интерпретатор и полный набор справочной документации с примерами.
Курс Udacity
Чтобы просмотреть курс Udacity по этой теме, см. Kotlin Bootcamp for Programmers .
IntelliJ IDEA
Документацию по IntelliJ IDEA можно найти на сайте JetBrains.
Стандартная библиотека Kotlin
Стандартная библиотека Kotlin предоставляет множество полезных функций. Прежде чем писать собственную функцию или интерфейс, всегда проверяйте стандартную библиотеку, чтобы убедиться, что кто-то не помог вам сэкономить время. Проверяйте её время от времени, так как новые функции добавляются часто.
Учебники по Kotlin
На сайте https://try.kotlinlang.org вы найдете подробные учебные пособия по Kotlin Koans, веб-интерпретатор и полный набор справочной документации с примерами.
Курс Udacity
Чтобы просмотреть курс Udacity по этой теме, см. Kotlin Bootcamp for Programmers .
IntelliJ IDEA
Документацию по IntelliJ IDEA можно найти на сайте JetBrains.
В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:
- При необходимости задавайте домашнее задание.
- Объясните учащимся, как следует сдавать домашние задания.
- Оцените домашние задания.
Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.
Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.
Ответьте на эти вопросы
Вопрос 1
В Kotlin SAM означает:
▢ Безопасное сопоставление аргументов
▢ Простой метод доступа
▢ Метод единого абстрактного метода
▢ Методология стратегического доступа
Вопрос 2
Какая из следующих функций не является функцией расширения стандартной библиотеки Kotlin?
▢ elvis()
▢ apply()
▢ run()
▢ with()
Вопрос 3
Что из перечисленного не относится к лямбда-выражениям в Kotlin?
▢ Лямбда-функции — это анонимные функции.
▢ Лямбда-выражения являются объектами, если они не встроены.
▢ Лямбда-выражения потребляют много ресурсов, поэтому их не следует использовать.
▢ Лямбда-выражения можно передавать другим функциям.
Вопрос 4
Метки в Kotlin обозначаются идентификатором, за которым следует:
▢ :
▢ ::
▢ @:
▢ @
Поздравляем! Вы завершили практическую работу по Kotlin Bootcamp for Programmers.
Обзор курса, включая ссылки на другие практические занятия, см. в статье «Kotlin Bootcamp for Programmers: Welcome to the course».