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 } ?: defaultValuerun:
виконує блок коду на об'єкті, переданому як this, і повертає результат цього блоку. Усередині блоку можна використовувати посилання на об'єкт через this.
val result = someObject?.run { property } ?: defaultValuewith:
виконує блок коду, який передає об'єкт як аргумент, і повертає результат цього блоку.
val result = with(someObject) { property } ?: defaultValueapply:
виконує блок коду на об'єкті, переданому як 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 bfun 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()