Kotlin Bootcamp для программистов 6: Функциональные манипуляции

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

  1. В src создайте новый пакет, example .
  2. В примере создайте новый файл Kotlin, Annotations.kt .

Шаг 2: Создайте свою собственную аннотацию

  1. В Annotations.kt создайте класс Plant с двумя методами: trim() и fertilize() .
class Plant {
        fun trim(){}
        fun fertilize(){}
}
  1. Создайте функцию, которая выводит все методы класса. Используйте ::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)
    }
}
  1. Создайте функцию main() для вызова вашей тестовой процедуры. Запустите программу и посмотрите на результат.
fun main() {
    testAnnotations()
}
⇒ trim
fertilize
  1. Создайте простую аннотацию ImAPlant .
annotation class ImAPlant

Это не делает ничего, кроме того, что указывает на наличие аннотации.

  1. Добавьте аннотацию перед классом Plant .
@ImAPlant class Plant{
    ...
}
  1. Измените функцию testAnnotations() , чтобы вывести все аннотации класса. Используйте функцию annotations для получения всех аннотаций класса. Запустите программу и посмотрите на результат.
fun testAnnotations() {
    val plantObject = Plant::class
    for (a in plantObject.annotations) {
        println(a.annotationClass.simpleName)
    }
}
⇒ ImAPlant
  1. Измените testAnnotations() , чтобы найти аннотацию ImAPlant . Используйте findAnnotation() чтобы найти конкретную аннотацию. Запустите программу и посмотрите на результат.
fun testAnnotations() {
    val plantObject = Plant::class
    val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
    println(myAnnotationObject)
}
⇒ @example.ImAPlant()

Шаг 3: Создайте целевую аннотацию

Аннотации могут быть нацелены на геттеры или сеттеры. В этом случае их можно применять с префиксом @get: или @set: :. Это часто встречается при использовании фреймворков с аннотациями.

  1. Объявите две аннотации: 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 можно пометить меткой. Метки представляют собой идентификатор, за которым следует символ @ .

  1. В 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. Запустите программу и посмотрите на результат.
⇒ 1 2 3 4 5 6 7 8 9 10 11 

Аналогично, можно использовать помеченный continue . Вместо того, чтобы выйти из помеченного цикла, помеченный continue переходит к следующей итерации цикла.

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

Шаг 1: Создайте простую лямбда-функцию

  1. Запустите REPL в IntelliJ IDEA, Инструменты > Kotlin > Kotlin REPL .
  2. Создайте лямбда-выражение с аргументом dirty: Int , которое выполняет вычисление, деля dirty на 2. Присвойте лямбда-выражение переменной waterFilter .
val waterFilter = { dirty: Int -> dirty / 2 }
  1. Вызовите waterFilter , передав значение 30.
waterFilter(30)
⇒ res0: kotlin.Int = 15

Шаг 2: Создание лямбда-фильтра

  1. Оставаясь в REPL, создайте класс данных Fish с одним свойством name .
data class Fish(val name: String)
  1. Составьте список из 3 Fish с именами Флиппер, Моби Дик и Дори.
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))
  1. Добавьте фильтр для проверки имен, содержащих букву «i».
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]

В лямбда-выражении it относится к текущему элементу списка, а фильтр применяется к каждому элементу списка по очереди.

  1. Примените joinString() к результату, используя ", " в качестве разделителя.
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick

Функция joinToString() создаёт строку, объединяя отфильтрованные имена, разделённые заданной строкой. Это одна из многих полезных функций, встроенных в стандартную библиотеку Kotlin.

Передача лямбда-функции или другой функции в качестве аргумента создаёт функцию высшего порядка. Фильтр выше — простой пример. filter() — это функция, которой передаётся лямбда-функция, определяющая способ обработки каждого элемента списка.

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

Шаг 1: Создайте новый класс

  1. В пакете примеров создайте новый файл Kotlin, Fish.kt
  2. В Fish.kt создайте класс данных Fish с одним свойством name .
data class Fish (var name: String)
  1. Создайте функцию fishExamples() . В fishExamples() создайте рыбу с именем "splashy" , все буквы — строчные.
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
}
  1. Создайте функцию main() , которая вызывает fishExamples() .
fun main () {
    fishExamples()
}
  1. Скомпилируйте и запустите программу, щёлкнув по зелёному треугольнику слева от main() . Результат пока отсутствует.

Шаг 2: Используйте функцию высшего порядка

Функция with() позволяет создать одну или несколько ссылок на объект или свойство более компактным способом. Используя this , with() фактически является функцией высшего порядка, и в лямбда-функции вы указываете, что делать с предоставленным объектом.

  1. Используйте with() для написания названия рыбы с заглавной буквы в fishExamples() . Внутри фигурных скобок this относится к объекту, переданному в with() .
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        this.capitalize()
    }
}
  1. Вывода нет, поэтому добавьте println() вокруг него. А this неявный и не нужен, поэтому его можно удалить.
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        println(capitalize())
    }
}
⇒ Splashy

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

По сути, with() — это функция высшего порядка. Чтобы увидеть, как это работает, вы можете создать собственную, значительно упрощённую версию with() , которая работает только со строками.

  1. В Fish.kt определите функцию myWith() , принимающую два аргумента. Аргументами являются объект, над которым выполняется операция, и функция, определяющая операцию. Имя аргумента для функции принято обозначать как block . В данном случае функция ничего не возвращает, что определяется с помощью Unit .
fun myWith(name: String, block: String.() -> Unit) {}

Внутри myWith() функция block() теперь является функцией расширения String . Расширяемый класс часто называется объектом-приёмником . Поэтому в данном случае name — это объект-приёмник.

  1. В теле myWith() примените переданную функцию block() к объекту-получателю name .
fun myWith(name: String, block: String.() -> Unit) {
    name.block()
}
  1. В fishExamples() замените with() на myWith() .
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    myWith (fish.name) {
        println(capitalize())
    }
}

fish.name — аргумент имени, а println(capitalize()) — блочная функция.

  1. Запустите программу, и она будет работать как и прежде.
⇒ Splashy

Шаг 4: Изучите другие встроенные расширения

Расширение with() очень полезно и входит в стандартную библиотеку Kotlin . Вот несколько других, которые могут вам пригодиться: run() , apply() и let() .

Функция run() — это расширение, работающее со всеми типами. Она принимает в качестве аргумента одно лямбда-выражение и возвращает результат его выполнения.

  1. В fishExamples() вызовите run() для fish , чтобы получить имя.
fish.run {
   name
}

Этот метод просто возвращает свойство name . Вы можете присвоить его переменной или вывести на экран. На самом деле, это не очень полезный пример, поскольку можно просто получить доступ к свойству, но run() может быть полезен для более сложных выражений.

Функция apply() похожа на run() , но возвращает изменённый объект, к которому она была применена, а не результат лямбда-выражения. Это может быть полезно для вызова методов для вновь созданного объекта.

  1. Создайте копию fish и вызовите apply() чтобы задать имя новой копии.
val fish2 = Fish(name = "splashy").apply {
     name = "sharky"
}
println(fish2.name)
⇒ sharky

Функция let() похожа на apply() , но возвращает копию объекта с изменениями. Это может быть полезно для объединения манипуляций в цепочку.

  1. Используйте let() , чтобы получить название fish , напишите его заглавными буквами, присоедините к нему другую строку, получите длину полученного результата, прибавьте к длине 31, а затем выведите результат.
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
⇒ 42

В этом примере тип объекта, на который it ссылается, — Fish , затем String , затем снова String и, наконец, Int .

  1. Выведите 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 в качестве параметров. Попробуйте пример ниже.

  1. Внутри примера создайте класс Java JavaRun и вставьте в файл следующее.
package example;

public class JavaRun {
    public static void runNow(Runnable runnable) {
        runnable.run();
    }
}

Kotlin позволяет создать экземпляр объекта, реализующего интерфейс, добавив к типу object: . Это полезно для передачи параметров в SAM.

  1. Вернитесь в Fish.kt , создайте функцию runExample() , которая создает Runnable с помощью object: Объект должен реализовать run() , выведя "I'm a Runnable" .
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
}
  1. Вызовите JavaRun.runNow() с созданным вами объектом.
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
    JavaRun.runNow(runnable)
}
  1. Вызовите runExample() из main() и запустите программу.
⇒ I'm a Runnable

Много работы, чтобы что-то вывести, но это хороший пример того, как работает SAM. Конечно, в Kotlin есть более простой способ сделать это — использовать лямбда-выражение вместо объекта, что делает код гораздо компактнее.

  1. Удалите существующий код в runExample , измените его так, чтобы он вызывал runNow() с лямбда-функцией, и запустите программу.
fun runExample() {
    JavaRun.runNow({
        println("Passing a lambda as a Runnable")
    })
}
⇒ Passing a lambda as a Runnable
  1. Вы можете сделать это еще более кратко, используя синтаксис вызова последнего параметра и избавившись от скобок.
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».