Roman Kryvolapov Engineering Blog

Kotlin Core – Основни въпроси

Какво са вътрешните класове (Inner Classes) в Kotlin

В Kotlin вложените класове (nested classes) са класове, които са декларирани вътре в друг клас. Ако такъв клас е означен с ключовата дума inner, той става вътрешен (inner class), има достъп до членовете на външния клас и може да се използва за реализиране на шаблони за проектиране, като „Стратегия“ или „Наблюдател“.

В Kotlin вложеният клас по подразбиране не пази препратка към външния клас (като статичен вложен клас в Java, за разлика от обикновения вложен клас в Java). Тоест той няма достъп до членовете на външния клас и може да бъде създаден без създаване на екземпляр на външния клас.

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

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.

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

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

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

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

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

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

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

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

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

могат да се комбинират

Kotlin
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) – неблокиращо забавяне

Kotlin
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{}:
не връща нищо,

Kotlin
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(), за да получим резултата.

Kotlin
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

Kotlin
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() с определена сигнатура

Kotlin
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() като аргумент, и запомня получената стойност, а следващите извиквания просто връщат вече изчислената стойност.

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

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

Kotlin
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

Съхранение на свойства в асоциативен списък:

Kotlin
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 позволява да се разширява клас чрез добавяне на нова функционалност.
Разширенията всъщност не правят никакви промени в класовете, които разширяват. Като декларирате разширение, вие създавате нова функция, а не нов член на класа. Такива функции могат да се извикват чрез точка, приложена към конкретен тип.
Разширенията имат статична диспечеризация: това означава, че извиканата функция за разширение се определя от типа на нейния израз по време на компилация, а не от типа на израза, изчислен по време на изпълнение на програмата, както при извикване на виртуални функции.

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

Какво е companion object в Kotlin

Нещо като заместител на статиката в Java; извикват се чрез името на класа. Такива членове на помощните обекти изглеждат като статични членове в други езици за програмиране. А всъщност те са членове на реални обекти и могат да реализират, например, интерфейси:

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

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

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, не връща нищо.

Kotlin
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 }
Като се използва екземпляр на потребителски клас, който реализира функционалния тип като интерфейс:

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

Copyright: Roman Kryvolapov