Перейти к содержимому

Вопросы и ответы — Kotlin Core

Все вопросы:

В 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"
}

В 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 }

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

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

Есть две функции для запуска корутины:
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{}:
ничего не возвращает,

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
}

Делегирование классов:
Ключевое слово 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
))

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

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

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

interface Factory<T> {
    fun create(): T
}
class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}
var stringRepresentation: String
    get() = this.toString()
    set(value) { 
      setDataFromString(value) 
    }
var setterVisibility: String = "abc" 
    private set // сеттер имеет private доступ и стандартную реализацию

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

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