Roman Kryvolapov Engineering Blog

Kotlin Core — Основные вопросы

Что такое внутренние классы (Inner Classes) в Kotlin

В Kotlin внутренние классы (inner classes) — это классы, которые объявлены внутри другого класса. Они имеют доступ к членам внешнего класса и могут использоваться для реализации паттернов проектирования, таких как “Стратегия” или “Наблюдатель”.

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

Однако, если внутренний класс помечен ключевым словом inner, то он становится нестатическим, и тогда он получает доступ к нестатическим членам внешнего класса и требует экземпляра внешнего класса для создания.
Пример внутреннего класса в Kotlin:

class Outer {
private val outerField = "Outer field"
inner class Inner {
fun innerMethod() {
println("Accessing outer field: $outerField")
}
}
}
fun main() {
val outer = Outer()
val inner = outer.Inner()
inner.innerMethod() // Выведет "Accessing outer field: Outer field"
}

Какие есть функции области видимости (Scope Functions) в Kotlin

В Kotlin есть пять функций области видимости (Scope Functions), которые позволяют изменять область видимости переменных, а также упрощают чтение кода и уменьшают вероятность ошибок:

let:
позволяет выполнить блок кода на объекте, переданном в качестве аргумента, и вернуть результат этого блока. Внутри блока можно использовать ссылку на объект через it.

val result = someObject?.let { it.property } ?: defaultValue

run:
выполняет блок кода на объекте, переданном в качестве this, и возвращает результат этого блока. Внутри блока можно использовать ссылку на объект через this.

val result = someObject?.run { property } ?: defaultValue

with:
выполняет блок кода, который передает объект в качестве аргумента, и возвращает результат этого блока.

val result = with(someObject) { property } ?: defaultValue

apply:
выполняет блок кода на объекте, переданном в качестве this, и возвращает этот объект. Внутри блока можно использовать ссылку на объект через this.

val someObject = SomeClass().apply {
property1 = value1
property2 = value2
}

also:
позволяет выполнить блок кода на объекте, переданном в качестве аргумента, и вернуть этот объект. Внутри блока можно использовать ссылку на объект через it.

val someObject = SomeClass().also {
it.property1 = value1
it.property2 = value2
}

их можно комбинировать

variable?.let { переменная не null } ?: run { переменная null }
// то же самое, что
if(variable != null) { переменная не null } else { переменная null }

Как работает корутина и что такое suspend

Обычная функция:
не име­ют сос­тояния и всег­да запус­кают­ся «с чис­того лис­та» (если, конеч­но, не исполь­зуют гло­баль­ные перемен­ные)
дол­жна завер­шить свое исполне­ние, перед тем как вер­нуть управле­ние выз­вавше­му ее коду

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

Есть две функции для запуска корутины:
launch{} и async{}.

launch{}:
ничего не возвращает

async{}:
возвращает экземпляр Deferred, в котором имеется функция await(), которая возвращает результат корутины

CoroutineScope:
отслеживает любую корутину, созданную с помощью launchили async (это функции расширения в CoroutineScope).
Текущая работа (запущенные корутины) может быть отменена вызовом scope.cancel() в любой момент времени.

Job:
управляющий корутиной элемент. Для каждой создаваемой корутины (с помощью launch или async) он возвращает экземпляр Job, который однозначно идентифицирует корутину и управляет ее жизненным циклом. Job может проходить через множество состояний: новое, активное, завершение, завершенное, отмена и отмененное. Хотя у нас нет доступа к самим состояниям, мы можем получить доступ к свойствам Job: isActive, isCancelled и isCompleted.

CoroutineContext:
это набор элементов, определяющих поведение корутины.
Он состоит из:
Job: управляет жизненным циклом корутины.
CoroutineDispatcher: отправляет работу в соответствующий поток.
CoroutineName: имя корутины, полезно для отладки.
CoroutineExceptionHandler: обрабатывает неотловленные исключения, которые будут рассмотрены в 3 части серии о корутинах.
В корутинах есть delay(1000L)- не блокирующая задержка

suspend fun doSomeWork() = coroutineScope {
launch { doWork() }
}
suspend fun doWork() {
println("before")
delay(400L)
println("after")
}

Поскольку в этой функции применяется функция delay(), то doWork() определена с модификатором suspend. Сама корутина создается также с помощью функции launch(), которая вызывает функцию doWork().
При вызове задержки с помощью функции delay(), эта корутина освобождает поток, в котором она выполнялась, и сохраняется в памяти. А освобожденный поток может быть задействован для других задач. А когда завершается запущенная задача (например, выполнение функции delay()), корутина возобновляет свою работу в одном из свободных потоков.

Какая разница между методом запуска корутин launch и async

Есть две функции для запуска корутины: launch{} и async{}.

launch{}:
ничего не возвращает,

fun main(args: Array<String>) {
print("1 ")
val job: Job = GlobalScope.launch {
print("3 ")
delay(1000L)
print("4 ")
}
print("2 ")
// 1 2 но дальше не выполнится так как программа закончится раньше
// либо можно сделать job.cancel()
}

async{}:
возвращает экземпляр Deferred, в котором имеется функция await(), которая возвращает результат корутины, прямо как Future в Java, где мы делаем future.get() для получения результата.

suspend fun main(args: Array<String>) {
print("1 ")
val deferred: Deferred<String> = GlobalScope.async {
return@async "3 "
}
print("2 ")
print(deferred.await())
print("4 ")
// 1 2 3 4
}

Что такое Делегаты в Kotlin

Делегирование классов:
Ключевое слово by в оглавлении Derived, находящееся после типа делегируемого класса, говорит о том, что объект b типа Base будет храниться внутри экземпляра Derived, и компилятор сгенерирует у Derived соответствующие методы из Base, которые при вызове будут переданы объекту b

interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // prints 10
}

Делегирование свойств:
Существует несколько основных видов свойств, которые мы реализовываем каждый раз вручную в случае их надобности. Однако намного удобнее было бы реализовать их раз и навсегда и положить в какую-нибудь библиотеку. Примеры таких свойств:
ленивые свойства (lazy properties): значение вычисляется один раз, при первом обращении
свойства, на события об изменении которых можно подписаться (observable properties)
свойства, хранимые в ассоциативном списке, а не в отдельных полях
Для таких случаев, Kotlin поддерживает делегированные свойства.
Выражение после by — делегат: обращения (get(), set()) к свойству будут обрабатываться этим выражением. Делегат не обязан реализовывать какой-то интерфейс, достаточно, чтобы у него были методы getValue() и setValue() с определённой сигнатурой

class Example {
var p: String by Delegate()
}
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, спасибо за делегирование мне '${property.name}'!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value было присвоено значению '${property.name} в $thisRef.'")
}
}

Стандартные делегаты:

Ленивые свойства (lazy properties):
первый вызов get() запускает лямбда-выражение, переданное lazy() в качестве аргумента, и запоминает полученное значение, а последующие вызовы просто возвращают вычисленное значение.

val lazyValue: String by lazy {
println("computed!")
"Hello"
}
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue) // computed! Hello
println(lazyValue) // Hello
}

Observable свойства:
Функция Delegates.observable() принимает два аргумента: начальное значение свойства и обработчик (лямбда), который вызывается при изменении свойства. У обработчика три параметра: описание свойства, которое изменяется, старое значение и новое значение.

class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
// вывод
// <no name> -> first
// first -> second

Хранение свойств в ассоциативном списке:

class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))

Что такое Extension в Kotlin

Kotlin позволяет расширять класс путём добавления нового функционала.
Расширения на самом деле не проводят никаких модификаций с классами, которые они расширяют. Объявляя расширение, вы создаёте новую функцию, а не новый член класса. Такие функции могут быть вызваны через точку, применимо к конкретному типу.
Расширения имеют статическую диспетчеризацию: это значит, что вызванная функция-расширение определяется типом её выражения во время компиляции, а не типом выражения, вычисленным в ходе выполнения программы, как при вызове виртуальных функций.

fun Any?.toString(): String {
if (this == null) return "null"
return toString()
}

Что такое companion object в Kotlin

Что то вроде замены статики в Java, к ним обращаются через название класса. Такие члены вспомогательных объектов выглядят, как статические члены в других языках программирования. На самом же деле, они являются членами реальных объектов и могут реализовывать, к примеру, интерфейсы:

interface Factory<T> {
fun create(): T
}
class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}

Как использовать геттеры (get) и сеттеры (set) в Kotlin

var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value)
}
var setterVisibility: String = "abc"
private set // сеттер имеет private доступ и стандартную реализацию

Что такое аннотация @JvmStatic

Указывает, что из этого элемента должен быть сгенерирован дополнительный статический метод,если это функция.Если этот элемент является свойством,то должны быть сгенерированы дополнительные статические методы получения/установки.

Что такое обратный вызов (callback), функциональные типы и Unit в Kotlin и как используется

Unit то же что и void в Java, не возвращает ничего.

fun main(args: Array<String>) {
one { variable ->
print(variable)
}
two { variable ->
print(variable)
return@two "4 "
}
three {
return@three "5 "
}
four({ variableOne ->
print(variableOne)
}, { variableTwo ->
print(variableTwo)
})
val listenerOne: (() -> Unit) = {
print("8 ")
}
listenerOne.invoke()
val listenerTwo: ((String) -> Unit) = { variable ->
print(variable)
}
listenerTwo.invoke("9 ")
val listenerThree: ((String) -> String) = { variable ->
variable
}
val result = listenerThree.invoke("10 ")
print(result)
}
fun one(callback: (String) -> Unit) {
// передаем в лямбду и передаем в нее String, ничего не ожидаем обратно (Unit)
callback("1 ")
}
fun two(callback: (String) -> String) {
// передаем в лямбду и передаем в нее String, ожидаем обратно String
callback("2 ")
print(callback("3 "))
}
fun three(callback: () -> String) {
// вызываем лямбду но ничего в нее не передаем, ожидаем обратно String
print(callback())
}
fun four(callbackOne: (String) -> Unit, callbackTwo: (String) -> Unit) {
// возвращает 2 коллбека
callbackOne("6 ")
callbackTwo("7 ")
}
// 1 2 3 4 5 6 7 8 9 10

Функциональные типы:
Kotlin использует семейство функциональных типов, таких как (Int) -> String, для объявлений, которые являются частью функций: val onClick: () -> Unit = ….

У всех функциональных типов есть список с типами параметров, заключенный в скобки, и возвращаемый тип: (A, B) -> C обозначает тип, который предоставляет функции два принятых аргумента типа A и B, а также возвращает значение типа C. Список с типами параметров может быть пустым, как, например, в () -> A. Возвращаемый тип Unit не может быть опущен.

У функциональных типов может быть дополнительный тип получатель (receiver), который указывается в объявлении перед точкой: тип A.(B) -> C описывает функции, которые могут быть вызваны для объекта-получателя A с параметром B и возвращаемым значением C. Литералы функций с объектом-приёмником часто используются вместе с этими типами.

Останавливаемые функции (suspending functions) принадлежат к особому виду функциональных типов, у которых в объявлении присутствует модификатор suspend, например, suspend () -> Unit или suspend A.(B) -> C.

Объявление функционального типа также может включать именованные параметры: (x: Int, y: Int) -> Point. Именованные параметры могут быть использованы для описания смысла каждого из параметров.

Чтобы указать, что функциональный тип может быть nullable, используйте круглые скобки: ((Int, Int) -> Int)?.

При помощи круглых скобок функциональные типы можно объединять: (Int) -> ((Int) -> Unit).

Стрелка в объявлении является правоассоциативной (right-associative), т.е. объявление (Int) -> (Int) -> Unit эквивалентно объявлению из предыдущего примера, а не ((Int) -> (Int)) -> Unit.

Вы также можете присвоить функциональному типу альтернативное имя, используя псевдонимы типов:
typealias ClickHandler = (Button, ClickEvent) -> Unit

Создание функционального типа:
Существует несколько способов получить экземпляр функционального типа:
Используя блок с кодом внутри функционального литерала в одной из форм:
лямбда-выражение: { a, b -> a + b },
анонимная функция: fun(s: String): Int { return s.toIntOrNull() ?: 0 }
Используя экземпляр пользовательского класса, который реализует функциональный тип в качестве интерфейса:

class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()

Copyright: Roman Kryvolapov