Kotlin Core – Основни въпроси
Какво са вътрешните класове (Inner Classes) в Kotlin
В Kotlin вложените класове (nested classes) са класове, които са декларирани вътре в друг клас. Ако такъв клас е означен с ключовата дума inner, той става вътрешен (inner class), има достъп до членовете на външния клас и може да се използва за реализиране на шаблони за проектиране, като „Стратегия“ или „Наблюдател“.
В Kotlin вложеният клас по подразбиране не пази препратка към външния клас (като статичен вложен клас в Java, за разлика от обикновения вложен клас в Java). Тоест той няма достъп до членовете на външния клас и може да бъде създаден без създаване на екземпляр на външния клас.
Но ако вложеният клас е означен с ключовата дума 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"}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()