JavaRush /Курсы /Kotlin SELF /Kotlin 2.x в проекте: версии, совместимость и дисциплина ...

Kotlin 2.x в проекте: версии, совместимость и дисциплина обновлений

Kotlin SELF
61 уровень , 3 лекция
Открыта

1. Слои версий в Kotlin‑проекте

Когда вы слышите «давай обновим Kotlin до 2.x», очень хочется представить себе одну ручку “Kotlin version”, которую мы повернули — и всё стало современным и красивым. На практике Kotlin в проекте живёт сразу в нескольких слоях: компилятор, Gradle‑плагин, стандартная библиотека, зависимости и IDE. И сломаться может любой слой, причём иногда он ломается очень вежливо: просто начинает ругаться предупреждениями, которые вы проигнорируете, а через полгода они станут ошибками.

Чтобы не путаться, давайте введём понятную «карту местности»: где какая версия живёт и за что отвечает. Самое полезное — научиться разделять: версия языка (какой синтаксис и правила доступны), версия компилятора (как это анализируется и во что превращается), и версия инструментов сборки (как проект вообще компилируется в CI и на вашей машине).

Таблица: какие версии есть в Kotlin‑проекте

Слой Что это такое человеческим языком Где обычно настраивается Типичные симптомы проблем
Версия Kotlin Gradle Plugin (KGP) Плагин, который добавляет задачи компиляции Kotlin в Gradle build.gradle.kts (plugins { kotlin("jvm") version "…" }) Сборка не стартует, падают Gradle‑таски, странные ошибки конфигурации
Версия компилятора Kotlin То, что реально компилирует код (включая K2‑фронтенд в Kotlin 2.x) Приходит вместе с KGP Код, который “вчера компилился”, сегодня не компилируется, диагностика меняется
Language/API version “Правила языка” и “уровень API”, с которым вы совместимы compilerOptions { languageVersionapiVersion} Неожиданные запреты/разрешения фич; несовпадение ожиданий между модулями
Стандартная библиотека (stdlib) Базовые функции/типы Kotlin Обычно подтягивается автоматически KGP, но может фиксироваться зависимостью Конфликты версий, NoSuchMethodError при запуске (редко, но бывает)
Плагины и кодогенерация (KSP и др.) Инструменты, которые участвуют в компиляции Gradle плагины + зависимости Генерация перестала работать, «несовместимая версия», падение на этапе сборки
IDE + Kotlin plugin в IDE То, что подсвечивает код и запускает сборку из кнопки IntelliJ IDEA «В IDE красное, но Gradle собирает» или наоборот

Эта таблица — как аптечка: может выглядеть скучно, но в момент боли она внезапно становится любимой.

2. K2, languageVersion и совместимость обновлений

Что такое K2 и почему после обновления «краснеет» код

Kotlin 2.x — это не только новые возможности языка, но и большой внутренний сдвиг: появился новый фронтенд компилятора (его часто называют K2). Если очень упростить, фронтенд — это часть компилятора, которая читает ваш код, понимает его смысл, строит внутреннее представление и решает: «вот это можно, а вот это нельзя». Когда фронтенд меняется, меняются и диагностики, и трактовка некоторых “угловых” случаев — иногда ваш код не стал хуже, просто компилятор стал строже и предсказуемее.

Важная мысль для начинающего (и для уставшего опытного тоже): если после обновления Kotlin у вас вылезли новые ошибки — это не обязательно “всё сломали”. Иногда это компилятор перестал терпеть то, что раньше молча проглатывал.

Пример: open val и инициализация в init

Вот показательный пример из миграции на Kotlin 2.0: раньше некоторые варианты отложенной инициализации “прокатывали”, теперь — нет. В Kotlin 2.0 open‑свойства с backing field должны иметь инициализацию сразу, иначе компилятор ругается.

open class Base {
    open val a: Int
    open var b: Int

    init {
        this.a = 1 // В Kotlin 2.0: ошибка, open val должен иметь initializer
        this.b = 1 // И это всегда было ошибкой для open var
    }
}

class Derived : Base() {
    override val a: Int = 2
    override var b: Int = 2
}

Почему это важно именно в лекции про «версии»? Потому что это классический случай: вы обновили Kotlin — и внезапно «сломался» код, который давно не трогали. А на самом деле просто изменилась трактовка правила, и проект теперь требует более явных гарантий.

Как проекту задать правила языка через languageVersion

Когда проект становится больше одной папки “с уроками”, вы начинаете жить в реальности, где часть кода уже написана “как привыкли”, часть — “как модно”, а часть — “как подсказала IDE в 2 часа ночи”. И в этот момент особенно важно уметь управлять правилами компиляции: иногда вы хотите обновить Gradle‑плагин и инструменты, но временно оставить старые правила языка, чтобы миграция прошла мягче. Или наоборот: хотите включить более новые правила там, где уже всё готово.

Ключевая идея: версия Kotlin‑плагина и версия правил языка — не одно и то же. Да, чаще всего они идут рядом, но иногда полезно временно “зафиксировать” languageVersion.

Пример: временно включить правила 1.9

В официальном гайде по миграции есть прямой рецепт: чтобы использовать предыдущий компилятор в Kotlin 2.0.0+ можно выставить languageVersion в 1.9 или передать -language-version 1.9.

Смысл этого шага не в том, чтобы “сидеть на 1.9 вечно”. Смысл — разделить проблему на куски: сначала обновили инструменты (Gradle, плагины), затем включили новые правила языка и разобрали ошибки.

Ниже — пример, как это может выглядеть в build.gradle.kts (упрощённо, идея важнее точного места в файле):

import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

tasks.withType<KotlinCompile>().configureEach {
    compilerOptions {
        languageVersion.set(KotlinVersion.KOTLIN_1_9)
        apiVersion.set(KotlinVersion.KOTLIN_1_9)
    }
}

Такой переключатель — как режим “временная запаска”. Не очень быстро, но позволяет доехать до сервиса, а не чинить колесо посреди трассы.

Почему Kotlin 2.x часто тянет за собой «паровоз» зависимостей

Есть два типа обновлений. Первый тип — “поменял цифру в одном месте и всё собралось”. Второй тип — “поменял цифру, и внезапно узнал новые слова: ABI, frontend, binary compatibility, plugin API”. Kotlin 2.x чаще относится ко второму типу, особенно если в проекте есть KSP, kotlinx.serialization или другие инструменты, которые завязаны на компиляцию.

Самая важная привычка, которую стоит выработать: если зависимость участвует в компиляции (а не только в рантайме), она чувствительнее к версии Kotlin. Это относится и к кодогенерации, и к компиляторным плагинам, и даже к тому, как IDE анализирует проект.

Схема зависимостей версий

flowchart TD
    A["Версия Kotlin Gradle Plugin (KGP)"] --> B["Версия компилятора Kotlin (K2/K1)"]
    A --> C["Задачи Gradle: compileKotlin, test, etc."]
    B --> D["Правила языка (languageVersion/apiVersion)"]
    B --> E["Инструменты компиляции: KSP/плагины"]
    E --> F["Сгенерированные .kt файлы"]
    F --> C

Идея проста: если вы обновили Kotlin, но забыли, что у вас есть KSP, который генерирует код, то вы обновили “двигатель”, но оставили старый “карбюратор”. Машина (сборка) может начать кашлять.

Практичный принцип совместимости

Если сформулировать максимально по‑человечески, то правило такое: обновляйте то, что связано с компиляцией, согласованно. Если вы обновляете Kotlin, посмотрите на:

  • Gradle‑плагин Kotlin (он же приносит компилятор);
  • KSP (если есть);
  • плагины сериализации и другие compile‑time инструменты (если есть);
  • и только потом — обычные библиотеки.

Здесь важно не “знать все версии наизусть”, а иметь дисциплину: обновляете один слой — проверяете сборку, фиксируете результат, идёте дальше.

3. Дисциплина обновлений и предупреждения компилятора

Пошаговый сценарий обновления без «всё горит»

В какой-то момент вы начнёте обновлять Kotlin не потому что “хочется новенького”, а потому что проекту нужен патч безопасности, или библиотека перестала поддерживать старую версию, или коллега прислал PR со словами “у меня локально всё работает” (что является древним заклинанием хаоса). И тут выигрывают не те, кто быстрее меняет цифры, а те, кто делает обновления предсказуемыми.

Дальше будет не “идеальный процесс на 40 страниц”, а жизненный, для небольшого учебного или pet‑проекта. И, что приятно, он масштабируется: те же идеи работают и в больших командах.

Сначала вы создаёте отдельную ветку. Это банально, но это момент, когда вы покупаете себе право на ошибку. Потом вы обновляете Kotlin Gradle plugin (обычно это строка версии в plugins {}). После этого вы сразу запускаете сборку из Gradle (не только из IDE), потому что IDE иногда живёт “в своём мире”, а Gradle — это тот мир, где живёт CI.

Если сборка упала, вы не бежите чинить всё подряд. Вы смотрите: это ошибка конфигурации Gradle (не собрался build‑скрипт), это ошибка компиляции Kotlin (не компилируется код), или это ошибка тестов (код компилируется, но поведение изменилось). Это три разных типа проблем, и их полезно разделять хотя бы у себя в голове.

Дальше вы включаете новые правила языка не сразу, а осознанно. Если при переходе на Kotlin 2.x проект сразу в K2‑режиме начинает ругаться на много мест, вы можете временно выставить languageVersion на 1.9, собрать проект, а затем вернуть 2.x и чинить уже “чистые” проблемы языка, а не одновременно язык + сборку. Сам факт возможности отката описан в гайде миграции.

И наконец — вы фиксируете изменения маленькими коммитами. Это не бюрократия, а помощь себе будущему: “обновили Kotlin”, “починили X”, “починили Y”. Тогда откат и анализ становятся простыми.

Почему предупреждения в Kotlin 2.x лучше не «глушить»

У компилятора есть два режима общения с вами: “всё хорошо” и “я тебя предупреждал”. Предупреждения, особенно deprecation‑предупреждения, часто воспринимаются как шум: мешают, подчёркивают код, портят настроение. Но в реальности это один из самых дешёвых способов узнать о будущих проблемах заранее.

Kotlin и его экосистема регулярно переводят предупреждения в ошибки по мере развития языка. Поэтому привычка “ой, да ладно, потом” иногда превращается в “почему CI красный, если мы ничего не меняли?”. Ответ обычно такой: вы не меняли код, но мир вокруг (версии) поменял правила.

Практичная стратегия здесь звучит скучно, но работает: если предупреждение можно исправить за 5–10 минут без потери смысла — исправляйте. Если нельзя, тогда хотя бы сделайте это решение осознанным: оставьте комментарий, заведите задачу, или примените подавление (@Suppress(...)) максимально локально. Иначе через полгода вы будете смотреть на подавления как археолог: “кто это сделал и почему здесь похоронен мамонт?”.

4. Миграция своего API через @Deprecated

Когда вы обновляете Kotlin, почти всегда всплывает второй пласт работы: ваш собственный код тоже развивается. Вы переименовываете функции, меняете контракты, убираете “костыли”, улучшаете дизайн API. И тут легко сделать больно пользователям вашего кода (или самому себе через месяц): вы переименовали метод — и всё приложение развалилось.

Kotlin даёт очень удобный механизм мягкой миграции: @Deprecated. Это буквально дорожный знак “тут ремонт, объезжай через новую улицу”. И самое приятное: IDE умеет подсказывать замену автоматически через ReplaceWith.

Пример: миграция функции через ReplaceWith

Представим, что в нашем учебном консольном приложении (условно назовём его StudyTracker) была функция formatMoneyOld(...), а мы хотим заменить её на formatMoney(...), потому что старое имя звучит как признание в грехах.

@Deprecated(
    message = "Используйте formatMoney(amountCents)",
    replaceWith = ReplaceWith("formatMoney(amountCents)")
)
fun formatMoneyOld(amountCents: Int): String = formatMoney(amountCents)

fun formatMoney(amountCents: Int): String {
    val dollars = amountCents / 100
    val cents = amountCents % 100
    return "$dollars.${cents.toString().padStart(2, '0')}"
}

Здесь важно, что старую функцию мы не удалили мгновенно: мы сделали её тонкой обёрткой, чтобы проект продолжал собираться, а IDE помогала переписать вызовы.

Пример: миграция свойства через делегирование

Иногда вы переименовываете не функцию, а свойство. Kotlin позволяет делегировать одно свойство другому, и это прямо рекомендуется как сценарий совместимого переименования: вводим новое свойство, старое помечаем @Deprecated и делегируем на новое.

class Settings {
    var currencyCode: String = "USD"

    @Deprecated("Use 'currencyCode' instead", ReplaceWith("currencyCode"))
    var currency: String by this::currencyCode
}

fun main() {
    val s = Settings()
    s.currency = "EUR"
    println(s.currencyCode) // EUR
}

Это выглядит почти как магия, но по смыслу очень просто: старое имя остаётся жить как “псевдоним”, а данные на самом деле хранятся в новом месте.

5. Типичные ошибки при обновлении Kotlin до 2.x

Ошибка №1: обновить Kotlin и ещё двадцать зависимостей “заодно”, а потом искать источник поломки.
Так делать очень хочется, потому что “раз уж страдать — то по полной программе”. Но это делает диагностику почти невозможной: вы не знаете, сломалось из‑за Kotlin, из‑за Gradle, из‑за плагина или из‑за библиотеки. Гораздо стабильнее обновлять слоями и фиксировать маленькие шаги.

Ошибка №2: путать “версию Kotlin” и “версию languageVersion”.
Версия Kotlin Gradle plugin приносит компилятор, но правила языка можно временно зафиксировать. В гайде миграции прямо упоминается вариант отката на languageVersion 1.9 или -language-version 1.9, чтобы использовать предыдущий компилятор. Это полезно как временная мера, но важно помнить, что это именно временный мост, а не “новая норма”.

Ошибка №3: игнорировать изменения семантики и правил, которые компилятор стал проверять строже.
На Kotlin 2.x можно внезапно получить ошибку там, где раньше был “серый” код. Пример с open val, который раньше мог инициализироваться в init, а теперь требует initializer, хорошо показывает природу таких изменений. Здесь неправильно злиться на компилятор — правильнее принять, что проекту нужны более явные гарантии.

Ошибка №4: ломать своё API “резко”, без мягкой миграции.
Удалить старую функцию, переименовать свойство и надеяться “ну я же всё найду поиском” — работает до первого пропущенного места. Kotlin даёт отличный механизм миграции через @Deprecated и даже через делегирование старого свойства на новое. Если вы этим не пользуетесь, вы добровольно отказываетесь от бесплатной помощи IDE и компилятора.

Ошибка №5: воспринимать предупреждения как мусор и глушить их глобально.
Предупреждения — это часто “ранняя версия ошибки из будущего”. Если их бездумно подавлять, проект становится хрупким: он компилируется “сейчас”, но не объясняет, что именно нужно поправить перед следующим обновлением. Лучше либо исправлять, либо документировать, почему подавление оправдано, и держать его локально.

1
Задача
Kotlin SELF, 61 уровень, 3 лекция
Недоступна
Починка инициализации
Починка инициализации
1
Задача
Kotlin SELF, 61 уровень, 3 лекция
Недоступна
Переименование свойства
Переименование свойства
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ