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