Irbisa · Шпаргалка

Продвинутый Kotlin

Дженерики, inline/reified, делегаты, DSL builder-ы, type alias

Middle-разработчик · 23 вопроса

  1. 01

    Что такое typealias, во что он компилируется и почему он не делает код типобезопасным?

    typealias задаёт просто второе имя для существующего типа: компилятор везде подставляет настоящий тип, поэтому в байткоде ничего нового не появляется, и UserId полностью взаимозаменяем с Long.

    typealias UserId = Longtypealias ClickHandler = (position: Int) -> Unittypealias Cache<K> = MutableMap<K, ByteArray> fun ban(id: UserId) { /* ... */ }val orderId: Long = 42L…
  2. 02

    Как работает вариантность в дженериках Kotlin (out, in, star projection) и как это соотносится с PECS из Java?

    По умолчанию generic-типы инвариантны; out T делает тип ковариантным, потому что он только отдаёт T, а in T делает его контравариантным, потому что он только принимает T.

    open class Animalclass Dog : Animal() interface Source<out T> { fun next(): T }        // producerinterface Sink<in T> { fun put(item: T) }        // consumer…
  3. 03

    Почему один объект Loading подходит для любого UiState<T> в этом коде и что сломается, если убрать out?

    sealed interface UiState<out T> {    data object Loading : UiState<Nothing>    data class Success<T>(val data: T) : UiState<T>    data class Error(val cause: Throwable) : UiState<Nothing>} val users: UiState<List<User>> = UiState.Loadingval profile: UiState<User> = UiState.Loading

    Nothing считается подтипом любого типа, а с out T это отношение переносится на контейнер, поэтому UiState<Nothing> является подтипом любого UiState<T>, и один синглтон Loading обслуживает все экраны.

  4. 04

    Что такое стирание типов и почему проверка is здесь не компилируется, а приведение компилируется и падает позже?

    fun handle(payload: Any) {    if (payload is List<String>) {           // compile error        println(payload.first().length)    }    val names = payload as List<String>      // warning: unchecked cast    println(names.first().length)            // ClassCastException here} handle(listOf(1, 2, 3))

    На JVM аргументы дженериков стираются: в рантайме объект знает только, что он List, поэтому is List<String> проверить невозможно (это ошибка компиляции), а as List<String> проверяет лишь часть List и пропускает элементы не того типа.

  5. 05

    Что делает inline в Kotlin, для чего нужны noinline и crossinline и когда inline становится плохой идеей?

    Тело inline-функции и переданные ей лямбды копируются в каждое место вызова, поэтому объект Function не создаётся и лишнего вызова нет; noinline оставляет конкретную лямбду настоящим объектом, а crossinline запрещает нелокальный return из лямбды, которая выполняется в другом контексте.

    inline fun <T> measure(label: String, block: () -> T): T {    val start = System.nanoTime()    return block().also { println("$label: ${System.nanoTime() - start} ns") }} fun firstAdult(users: List<User>): User? {…
  6. 06

    Этот inline-хелпер не компилируется: от чего вас защищает компилятор и какие есть два способа это исправить?

    inline fun runOnUi(block: () -> Unit) {    Handler(Looper.getMainLooper()).post {        block()    }} fun save() {    runOnUi { if (isFinishing) return }}

    Лямбда встраивается в save(), где голый return означает «выйти из save», но post выполнит её позже внутри Runnable, когда save давно завершилась, поэтому компилятор отказывает: «Can't inline 'block' here: it may contain non-local returns».

  7. 07

    Что такое reified, почему он работает только с inline-функциями и можно ли вызвать такую функцию из Java?

    reified делает параметр типа доступным в рантайме: тело inline-функции копируется в каждое место вызова, и компилятор подставляет туда конкретный тип, поэтому работают T::class, is T и as T.

    inline fun <reified T : Activity> Context.launch(vararg extras: Pair<String, Any?>) {    startActivity(Intent(this, T::class.java).putExtras(bundleOf(*extras)))} inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? =    BundleCompat.getParcelable(this, key, T::class.java)…
  8. 08

    Как работают делегаты свойств и когда использовать lazy, observable, vetoable, notNull или by map?

    val x by d передаёт геттер и сеттер свойства объекту d: компилятор хранит d в скрытом поле и превращает каждое чтение в d.getValue(this, ::x), а каждую запись превращает в d.setValue(this, ::x, value).

    class ProfileViewModel : ViewModel() {    private val formatter by lazy { DateTimeFormatter.ofPattern("dd.MM.yyyy") }     var name: String by Delegates.observable("") { _, old, new ->        Log.d("Profile", "name: $old -> $new")    }…
  9. 09

    Потокобезопасен ли by lazy и что меняют режимы LazyThreadSafetyMode?

    По умолчанию да: lazy { } использует SYNCHRONIZED, поэтому инициализатор выполняется ровно один раз, даже если несколько потоков читают свойство одновременно; два других режима жертвуют этой гарантией ради меньшего числа блокировок.

    class ImageLoader(private val context: Context) {    // Default SYNCHRONIZED: safe to read from any thread    val client: OkHttpClient by lazy { buildOkHttpClient(context) }     // Read only on the main thread, so skip the lock    val placeholder: Drawable by lazy(LazyThreadSafetyMode.NONE) {…
  10. 10

    Найдите баг: add() кладёт товар в корзину, но экран не перерисовывается; его обновляет только clear().

    class CartStore(private val onChange: (List<String>) -> Unit) {    var items: MutableList<String> by Delegates.observable(mutableListOf()) { _, _, new ->        onChange(new)    }        private set     fun add(item: String) {        items.add(item)    }     fun clear() {        items = mutableListOf()    }}

    Delegates.observable срабатывает только при переприсваивании свойства, а items.add(item) меняет тот же список через геттер, поэтому колбэк не вызывается и экран пропускает каждое добавление; clear() работает лишь потому, что присваивает новый список.

  11. 11

    Что такое value class в Kotlin и в каких случаях он всё равно создаёт объект-обёртку?

    @JvmInline value class UserId(val raw: Long) задаёт отдельный тип на этапе компиляции, но в рантайме компилятор везде, где может, использует исходный Long напрямую, так что вы получаете типобезопасность без объекта на каждое значение.

    @JvmInlinevalue class UserId(val raw: Long) @JvmInlinevalue class Email(val value: String) {    init { require('@' in value) { "Invalid email: $value" } }…
  12. 12

    Что дают Java-коду аннотации @JvmStatic, @JvmOverloads, @JvmField и @JvmName?

    Они меняют JVM-форму Kotlin-объявления, чтобы Java могла вызывать его естественно: настоящий статический метод, перегрузки, которые подразумевают аргументы по умолчанию, обычное поле или другое JVM-имя.

    @file:JvmName("StringUtils")package com.app.util fun slugify(s: String) = s.lowercase().replace(' ', '-')   // Java: StringUtils.slugify(...) class Config private constructor(val host: String) {…
  13. 13

    Свойство с не-null типом String в рантайме оказалось null и уронило приложение: как null прошёл мимо компилятора?

    Null safety в Kotlin покрывает только код, который проверил компилятор Kotlin; null просачивается через платформенные типы Java, десериализацию на рефлексии, !! и lateinit, непроверяемые дженерики и порядок инициализации.

  14. 14

    Как в Kotlin работают аннотации и кодогенерация: что определяет retention и зачем переходить с kapt на KSP?

    Аннотация представляет собой метаданные на объявлении; её retention определяет, где она сохранится (только в исходниках, в class-файле или для рефлексии в рантайме), а annotation processors читают её на этапе компиляции и генерируют код.

    @Retention(AnnotationRetention.RUNTIME)@Target(AnnotationTarget.FUNCTION)annotation class Route(val path: String) // before: kapt(libs.room.compiler)// after:  ksp(libs.room.compiler)
  15. 15

    Почему компилятор требует @OptIn(...) перед вызовом некоторых API и как объявить свой opt-in маркер?

    Opt-in помечает API как экспериментальный или опасный: автор вешает на него аннотацию-маркер, и каждый вызывающий должен явно подтвердить этот маркер в коде, так что ломающее изменение затронет только тех, кто принял риск.

    @RequiresOptIn(    message = "Sync API is experimental and may change without notice.",    level = RequiresOptIn.Level.ERROR,)@Retention(AnnotationRetention.BINARY)@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)…
  16. 16

    Вы сделали публичную функцию библиотеки inline: она перестала компилироваться с internal-хелпером, а старые вызывающие продолжают выполнять прежнее тело. Почему?

    Тело inline-функции копируется в каждое место вызова, поэтому становится частью опубликованной бинарной поверхности: оно может обращаться только к тому, что видно вызывающему, а вызывающие хранят ту копию, с которой были скомпилированы, пока их не пересоберут.

    internal val defaultJson = Json { ignoreUnknownKeys = true } // error: Public-API inline function cannot access non-public-API 'defaultJson'// inline fun <reified T> decode(text: String): T = defaultJson.decodeFromString(text) @PublishedApi…
  17. 17

    Найдите баг: через какое-то время все остальные потоки, вызывающие refresh(), навсегда блокируются на этом локе.

    inline fun <T> ReentrantLock.guarded(block: () -> T): T {    lock()    return block().also { unlock() }} class TokenCache(private val api: Api) {    private val lock = ReentrantLock()    private var token: String? = null     fun refresh() {        lock.guarded {            if (token != null) return // already fresh            token = api.fetchToken()        }    }}

    guarded отпускает лок, только если block завершился нормально.

  18. 18

    Ваша inline fun <reified T> fromJson(s: String): T для fromJson<List<User>>() возвращает список LinkedTreeMap: где именно заканчиваются возможности reified?

    reified захватывает класс из места вызова, но не его аргументы типа: для T = List<User> T::class даёт просто List, поэтому Gson получает указание собрать список и наполняет его своими LinkedTreeMap по умолчанию, а ClassCastException вылетает там, где вы впервые читаете поле.

    // wrong: T::class is the raw class, the User argument is goneinline fun <reified T : Any> fromJson(json: String): T =    gson.fromJson(json, T::class.java) val users: List<User> = fromJson(body)println(users[0].name)   // ClassCastException: LinkedTreeMap cannot be cast to User…
  19. 19

    Два свойства одного класса, объявленные как by shared, в итоге хранят одно и то же значение: что на самом деле хранит by и как делегату понять, какое свойство он обслуживает?

    by expr вычисляет выражение один раз для каждого свойства и сохраняет результат в скрытом поле, поэтому два свойства с одним и тем же объектом-делегатом делят все его поля; в момент вызова их различает только KProperty, переданный в getValue/setValue.

    class Setting<T>(private val default: T) : ReadWriteProperty<Any?, T> {    private var cached: T? = null                          // state lives in the DELEGATE    override fun getValue(thisRef: Any?, property: KProperty<*>): T = cached ?: default    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { cached = value }}…
  20. 20

    Как построить типобезопасный DSL в Kotlin и от чего защищает @DslMarker?

    DSL на Kotlin пишется обычным кодом на лямбдах с ресивером: функция принимает block: Builder.() -> Unit, создаёт билдер, выполняет блок с билдером в роли this и возвращает результат, а @DslMarker не даёт вложенным блокам незаметно вызывать внешний билдер.

    @DslMarkerannotation class MenuDsl data class Item(val title: String, val icon: Int?, val onClick: () -> Unit)data class Menu(val items: List<Item>)…
  21. 21

    Почему smart cast сохраняется после if (!s.isNullOrEmpty()), но пропадает при вызове такого же хелпера, написанного вами?

    Функция стандартной библиотеки объявляет контракт (обещание компилятору о том, что её возвращаемое значение говорит об аргументах), а ваш хелпер для компилятора непрозрачен, поэтому он не знает, что false означает «не null», и отбрасывает smart cast.

    @OptIn(ExperimentalContracts::class)fun String?.isValidToken(): Boolean {    contract { returns(true) implies (this@isValidToken != null) }    return this != null && length == 32}…
  22. 22

    Что такое context parameters в Kotlin и какую проблему они решают, которую не решает дополнительный аргумент функции?

    Context parameter задаёт зависимость, которую функция объявляет, но которую никто не передаёт явно: компилятор подставляет её из подходящего контекста, уже доступного в области видимости, поэтому «фоновые» вещи вроде логгера или транзакции больше не нужно протаскивать через каждую сигнатуру.

    interface Logger { fun info(msg: String) }interface Analytics { fun track(event: String) } context(logger: Logger)fun sync() {    logger.info("sync started")…
  23. 23

    Что изменилось с компилятором K2 и Kotlin 2.x и что это значит для Android-проекта?

    Kotlin 2.0 сделал K2 (переписанный фронтенд компилятора) версией по умолчанию: более быстрый анализ и более умные smart cast почти без изменений в исходниках, а релизы 2.x с тех пор используют его, чтобы регулярно выпускать новые возможности языка.

    sealed interface Response {    data class Ok(val body: String) : Response    data class Failure(val code: Int, val retryable: Boolean) : Response} // Guard condition in when (stable in 2.2)…