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

Синтаксические конструкции Kotlin

Разработчику Архитектору

Синтаксис Kotlin

Kotlin — язык статической типизации, разработанный компанией JetBrains с 2011 года и официально признанный Google основным языком для Android-разработки в 2017 году. Его синтаксис формировался под влиянием широкого спектра языков — Java (совместимость и экосистема), Scala (мощные абстракции), C# (свойства, делегаты, асинхронность), Python и Groovy (лаконичность, читаемость) — однако итоговый результат не является простым миксом. Вместо этого Kotlin последовательно реализует принцип практической выразительности: каждая синтаксическая единица решает конкретную инженерную задачу, устраняя избыточность без жертвования надёжностью и производительностью.

Синтаксис Kotlin — рефакторинг парадигмы — замена шаблонных идиом, склонных к ошибкам (например, null-проверки, шаблонные классы данных, повторяющиеся шаблоны инициализации), на встроенные, проверяемые на этапе компиляции конструкции. Лаконичность достигается не за счёт умолчаний, ведущих к неопределённости, а за счёт вывода типов, интеллектуального умолчания, структурированной неизменяемости и семантически обогащённой грамматики.

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


1. Объявление переменных

В Kotlin отсутствует ключевое слово final, привычное по Java. Вместо этого используется двухуровневая система деклараций:

  • val (от value) — объявляет неизменяемую ссылку.
  • var (от variable) — объявляет изменяемую ссылку.

Это сознательный дизайн-решение, направленное на смещение баланса в сторону иммутабельности как стандартного подхода. Переменная, объявленная через val, не может быть переприсвоена — ни явно (val x = 5; x = 6 — ошибка компиляции), ни неявно (например, в цикле for). При этом объект, на который ссылается val, может быть изменяемым — например, val list = mutableListOf(1, 2, 3); list.add(4) допустимо. То есть val фиксирует саму ссылку, а не состояние объекта.

Тип может быть указан явно (val name: String = "Alice"), но в подавляющем большинстве случаев компилятор выводит его из инициализатора (val age = 30Int). Это не "динамическая типизация" — тип фиксируется на этапе компиляции, и попытка присвоить age = "thirty" вызовет ошибку. Вывод типов устраняет избыточность без потери строгой проверки.

Примечательно, что Kotlin допускает отложенную инициализацию (lateinit var) и делегированные свойства (by lazy, by Delegates.observable), но эти механизмы явно маркируются и требуют осознанного использования — в отличие от неявной отложенной инициализации в Java (где ссылка по умолчанию null), что является частой причиной NullPointerException.


2. Функции

Функции в Kotlin объявляются ключевым словом fun. Синтаксис включает:

  • Имя функции;
  • Список параметров в круглых скобках, каждый с именем и типом (обязательно);
  • Необязательный возвращаемый тип после двоеточия;
  • Тело — либо блок {}, либо одно выражение после =.
fun greet(name: String): Unit {
println("Hello, $name")
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Ключевые вызовы во фрагменте (greet, println) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Обратите внимание на возвращаемый тип Unit. Это аналог void в Java, но с принципиальным отличием: Unit — это реальный тип, имеющий ровно одно значение — Unit. Это позволяет трактовать все функции как возвращающие значение, что упрощает обобщённое программирование (например, в обобщённых контекстах, где требуется возвращаемый тип). При этом при записи Unit можно опустить — компилятор подставит его автоматически, если тело — блок без явного return.

Если функция состоит из одного выражения, её можно записать в выражении-функции (expression body):

fun square(x: Int) = x * x

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Ключевые вызовы во фрагменте (square) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Здесь возвращаемый тип выводится как Int. Такая форма особенно удобна для простых преобразований, делегирования и extension-функций.

Параметры функций всегда имеют имена и типы. Передача по имени (greet(name = "Alice")) поддерживается, что повышает читаемость при вызове с несколькими параметрами одинакового типа.


3. Строка и интерполяция

В Kotlin строки поддерживают интерполяцию — подстановку значений выражений непосредственно в текстовую константу:

val name = "Alice"
println("Hello, $name") // → Hello, Alice
println("Length: ${name.length}") // → Length: 5

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Ключевые вызовы во фрагменте (println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Символ $ не является строковым макросом или препроцессорной конструкцией — это часть лексического анализа строковых литералов. После $ допускается либо простое имя переменной, либо произвольное выражение в фигурных скобках. Это безопаснее, чем конкатенация ("Hello, " + name), так как исключает случайное пропускание пробела или непарных кавычек, и выразительнее — особенно при вложенных структурах.

Строки в Kotlin неизменяемы (String — финальный класс), как и в Java, но стандартная библиотека предоставляет богатый набор функций-расширений (trim(), split(), replace(), lines(), take(), drop(), регулярные выражения и др.), что делает работу с ними близкой по удобству к Python.


4. Null Safety

Одна из самых значимых инноваций Kotlin — система null-безопасности, реализованная на уровне системы типов. В отличие от Java, где любой объектный тип может принимать значение null неявно, Kotlin разделяет типы на не nullable и nullable:

  • String — не может быть null;
  • String? — может быть null.

Это изменение кардинально: компилятор запрещает вызов методов или доступ к свойствам nullable-типа без явной проверки. Существует несколько стандартных способов безопасной работы с nullable-значениями:


4.1. Оператор безопасного вызова ?.

val length = nullableName?.length // Int? — будет null, если nullableName == null

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Если левый операнд null, выражение возвращает null, не вызывая исключения.


4.2. Оператор элвиса ?:

Позволяет задать значение по умолчанию:

val len = nullableName?.length ?: 0
println(nullableName?.length ?: "No name")

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Выражение справа от ?: вычисляется только если левое — null. Это аналог Optional.orElse() в Java, но без накладных расходов на обёртку.


4.3. Утверждение non-null !!

val len = nullableName!!.length // NullPointerException, если null

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Используется сознательно — когда разработчик гарантирует ненулевое значение, а компилятор не может это проверить (например, при взаимодействии с Java-кодом). Это сигнал о ручной ответственности.


4.4. Безопасное приведение и проверки с is, as?

if (obj is String) {
println(obj.length) // obj автоматически приведён к String в этом блоке
}
val str = obj as? String // возвращает String? или null

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • if выполняет проверку условия и направляет выполнение в соответствующую ветку.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Это устраняет необходимость в повторных проверках и явных приведениях, сокращая шаблонный код.

Важно: система null-безопасности работает статически. Ошибки вида "возможно null" обнаруживаются на этапе компиляции, а не во время выполнения. Это не абсолютная гарантия (например, при работе с Java-кодом без @Nullable/@NonNull аннотаций), но радикально снижает долю NPE в "чистом" Kotlin-коде.


5. Data-классы

В Java класс, представляющий данные (например, User), требует ручной реализации или генерации equals(), hashCode(), toString(), copy(), геттеров/сеттеров. В Kotlin достаточно:

data class User(val id: Int, val name: String)

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • data class автоматически генерирует методы сравнения и копирования, поэтому модель удобна для DTO и сериализации.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (User) формируют основной поток выполнения и обмена данными.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Ключевое слово data автоматически генерирует:

  • equals() и hashCode() на основе всех свойств в первичном конструкторе;
  • toString() вида User(id=1, name=Alice);
  • copy() — метод для создания модифицированной копии:
    val user2 = user1.copy(name = "Bob");
  • компонентные функции component1(), component2() — для деструктурирования:
    val (id, name) = user.

Это не магия — код генерируется на этапе компиляции и доступен в байткоде. data-классы не могут быть абстрактными, открытыми (open), наследоваться от других классов (кроме Any) и не должны объявлять дополнительные свойства в теле, влияющие на семантику равенства (хотя технически могут — с предостережением компилятора).

data-классы — инструмент для транспортных и хранящих структур — DTO, модели, конфигурационные записи. Они фокусируются на идентичности по содержимому, а не по ссылке.


6. Extension-функции и свойства

Одна из наиболее мощных возможностей Kotlin — расширение существующих классов без изменения их исходного кода и без наследования. Это достигается через extension-функции и extension-свойства:

fun String.addExclamation() = this + "!"
val String.lastChar: Char get() = this.last()

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (addExclamation, get, last) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Эти объявления компилируются в статические методы (в случае JVM — в static final методы класса-хоста), принимающие экземпляр расширяемого типа как первый параметр (в данном случае — this). При вызове:

"Hello".addExclamation() // → "Hello!"
println("World".lastChar) // → 'd'

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (addExclamation, println) формируют основной поток выполнения и обмена данными.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

— создаётся иллюзия, что метод/свойство принадлежит классу, хотя на уровне JVM это обычный вызов статического метода.

Важные ограничения и особенности:

  • Extension-функция не имеет доступа к private или protected членам расширяемого класса — только к публичному API.
  • При наличии конфликта имён (например, extension-функция и метод класса с одинаковой сигнатурой), приоритет имеет метод класса.
  • Extensions не наследуются — они привязаны к статическому типу переменной, а не к её динамическому типу:
open class A
class B : A()

fun A.foo() = "A"
fun B.foo() = "B"

val b: A = B()
println(b.foo()) // → "A", несмотря на то, что b — экземпляр B

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (A, foo, B, println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

Это следствие статической диспетчеризации — и оно предсказуемо.

Extensions позволяют:

  • Организовывать вспомогательный код по семантическим модулям, а не по иерархии наследования;
  • Создавать DSL (Domain-Specific Languages), например, для конфигурации, тестирования, построения UI;
  • Улучшать API сторонних библиотек без wrapper-классов.

7. Управление потоком выполнения

В Kotlin практически все управляющие конструкции — выражения, возвращающие значение. Это устраняет необходимость в отдельных переменных-аккумуляторах и делает код более декларативным.


7.1. if как выражение

В отличие от Java, где if — это исключительно оператор, в Kotlin if всегда возвращает значение — результат последнего выражения в выбранной ветке:

val max = if (a > b) a else b

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • if выполняет проверку условия и направляет выполнение в соответствующую ветку.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Каждая ветка (then, else) должна быть выражением одного типа (или Unit). Пустой блок не допускается — это исключает ошибки вида "забыл else". При многострочных ветках используется блочная форма:

val description = if (score >= 90) {
println("Высокий результат")
"Отлично"
} else if (score >= 60) {
"Удовлетворительно"
} else {
"Неудовлетворительно"
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • if выполняет проверку условия и направляет выполнение в соответствующую ветку.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Компилятор проверяет, что все ветви возвращают совместимые типы. Если одна ветвь — String, а другая — Int, будет ошибка. Это гарантирует согласованность логики на уровне типов.


7.2. when — обобщённое сопоставление с образцом

when заменяет switch из Java, но значительно мощнее. Он поддерживает:

  • Сопоставление по значению (включая диапазоны, списки, условия);
  • Проверку типов (is);
  • Выражения в ветках;
  • Использование как выражения и как оператора.

Примеры:

val result = when (x) {
0 -> "Ноль"
in 1..10 -> "Маленькое число"
!in 11..100 -> "Вне диапазона"
is String -> "Строка длиной ${x.length}"
else -> "Что-то другое"
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • when группирует ветвление в одном выражении и делает проверку сценариев более прозрачной.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Ветки проверяются сверху вниз; выполнение останавливается на первой совпавшей. Нет необходимости в break — это исключает "проваливание" (fall-through), характерное для Java.

when может использоваться и без аргумента — тогда каждая ветка содержит логическое выражение:

when {
x < 0 -> println("Отрицательное")
x == 0 -> println("Ноль")
else -> println("Положительное")
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • when группирует ветвление в одном выражении и делает проверку сценариев более прозрачной.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Это эквивалент if-else if-else, но в более структурированной форме — особенно удобно при большом числе условий.

Важно — when должен быть исчерпывающим в контексте выражения — либо охватывать все возможные значения (например, все значения enum), либо содержать else. Компилятор проверяет полноту покрытия для sealed class и enum, что делает when ключевым инструментом в функционально-ориентированном стиле.


7.3. Циклы — for, while, do-while и range-based итерации

Цикл for в Kotlin унифицирован и работает только по итерируемым (Iterable<T>) или диапазонам (ClosedRange<T>). Нет синтаксиса for (int i = 0; ...) — вместо этого используются:

  • Диапазоны: 1..10 (включительно), 1 until 10 (исключая 10), 10 downTo 1, 1..10 step 2;
  • Функции-расширения: indices, withIndex().

Примеры:

for (i in 1..5) println(i) // 1, 2, 3, 4, 5
for (i in 1 until 5) println(i) // 1, 2, 3, 4
for (i in 5 downTo 1) println(i) // 5, 4, 3, 2, 1
for (i in 1..10 step 3) println(i) // 1, 4, 7, 10

val list = listOf("a", "b", "c")
for (item in list) println(item)
for ((index, value) in list.withIndex()) {
println("$index: $value")
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Циклическая конструкция в примере показывает повторяемую обработку элементов или шагов алгоритма.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Ключевые вызовы во фрагменте (println, listOf, withIndex) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

Циклы while и do-while сохраняют привычную семантику, но их использование поощряется только при условных итерациях, где нельзя предсказать число шагов (например, чтение из потока).

Диапазоны (IntRange, CharRange, LongRange) — это полноценные классы с методами (contains, isEmpty, first, last), реализующие Iterable. Они работают с любыми типами, реализующими Comparable, а также поддерживают пользовательские шаги через step.


8. Функции высшего порядка и лямбды

Kotlin поддерживает функции высшего порядка — функции, принимающие другие функции в качестве параметров или возвращающие их. Это фундамент асинхронного, реактивного и функционального стилей программирования.


8.1. Типы функций

Тип функции в Kotlin записывается как (T1, T2, ...) -> R, где T1... — типы параметров, R — возвращаемый тип.

Примеры:

  • () -> Unit — функция без параметров и возврата (аналог Runnable);
  • (Int) -> String — функция, принимающая Int и возвращающая String;
  • (String, String) -> Boolean — компаратор.

Такой тип может использоваться как аннотация параметра:

fun performOperation(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}

val sum = performOperation(3, 4) { a, b -> a + b }

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (performOperation, operation) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

8.2. Лямбда-выражения

Лямбда — анонимная функция, записываемая в фигурных скобках:

{ x: Int -> x * x }
{ a, b -> a + b }
{ println("Hello") }

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (println) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Особенности:

  • Типы параметров можно опустить, если они выводятся из контекста (как в примере выше);
  • Последнее выражение — возвращаемое значение;
  • Лямбда может захватывать переменные из окружающей области (closure), при этом val-переменные захватываются по значению, var — по ссылке (через обёртку в Ref).

Если лямбда — последний аргумент функции, её можно вынести за скобки:

list.map { it.length }
list.filter { it.startsWith("A") }.sortedBy { it }

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • Ключевые вызовы во фрагменте (startsWith) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Если функция принимает ровно одну лямбду, скобки можно опустить полностью.


8.3. it — неявный параметр

В лямбде с одним параметром можно использовать неявное имя it:

list.map { it.uppercase() }

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • Ключевые вызовы во фрагменте (uppercase) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Это не глобальная переменная — it вводится локально и привязан к единственному параметру. При вложенных лямбдах внешний it недоступен — нужно явно именовать параметры.


8.4. Inline-функции

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

Решение — inline:

inline fun <T> measureTime(block: () -> T): T {
val start = System.nanoTime()
val result = block()
val end = System.nanoTime()
println("Время: ${(end - start) / 1_000_000} мс")
return result
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Ключевые вызовы во фрагменте (measureTime, nanoTime, block, println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

При компиляции вызов measureTime { ... } заменяется встроенным кодом тела функции с подстановкой лямбды — как макрос, но с проверкой типов. Это устраняет аллокацию объекта и вызов виртуального метода.

Ограничения:

  • inline-функции не могут вызываться рекурсивно;
  • Лямбды внутри inline нельзя сохранять в поля (val f = block — ошибка), так как они не существуют как объекты во время выполнения;
  • Для таких случаев используется noinline или crossinline.

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


9. Обобщения (Generics)

Kotlin наследует систему обобщений от Java, но устраняет ряд неудобств через variance-аннотации и reified-типы.


9.1. Проблема неизменяемости обобщённых типов

В Java List<String> не является подтипом List<Object> — несмотря на то, что String — подтип Object. Это называется инвариантностью. В Kotlin это выражается явно:

interface MutableList<T> // инвариантный — можно и читать, и писать
interface List<out T> // ковариантный — только читать
interface Comparable<in T> // контравариантный — только писать

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Пример:

fun printAll(strings: List<String>) {
// List<out String> — ковариантен
val anys: List<Any> = strings // OK
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (printAll) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Это позволяет писать более гибкие API без ? extends T / ? super T (wildcards), которые в Java усложняют сигнатуры.


9.2. Reified-типы

Из-за type erasure в JVM обобщённый тип недоступен во время выполнения. Kotlin частично обходит это ограничение с помощью reified:

inline fun <reified T> List<*>.filterIsInstance(): List<T> {
return this.filter { it is T } as List<T>
}

val numbers = list.filterIsInstance<Int>()

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (filterIsInstance) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Только inline-функции могут иметь reified-параметры. При встраивании компилятор подставляет конкретный тип (Int), и it is T становится it is Int — безопасной проверкой.

Это широко используется в сериализации, DI-контейнерах, тестовых фреймворках.


10. Делегирование

Делегирование — один из ключевых принципов проектирования ("предпочитай композицию наследованию"). Kotlin встраивает его на синтаксический уровень.


10.1. Делегирование классов через by

Код ITЗагрузка примера кода…

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Ключевые вызовы во фрагменте (print, println, LoggingPrinter) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

Здесь LoggingPrinter делегирует все методы Printer экземпляру impl, кроме print, который переопределён. Это устраняет необходимость в ручном пробрасывании методов.


10.2. Делегированные свойства через by

Синтаксис val/var name: Type by Delegate() позволяет вынести логику чтения/записи в отдельный объект — делегат.

Стандартные делегаты:

  • lazy { ... } — отложенная инициализация (thread-safe по умолчанию):
val config by lazy { loadConfig() }

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (loadConfig) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.
var name: String by Delegates.observable("Anonymous") { _, old, new ->
println("Имя изменено: $old$new")
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • var используется там, где состояние действительно меняется во времени, например при обновлении данных.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Ключевые вызовы во фрагменте (observable, println) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Кастомный делегат реализует интерфейсы ReadOnlyProperty или ReadWriteProperty. Это позволяет создавать DSL для конфигурации, кэширования, привязки к UI и т.д.

Пример: делегат для преобразования строки в целое:

class IntDelegate {
private var value: Int = 0
operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
value = newValue.toInt()
}
}

var strAsInt: Int by IntDelegate()
strAsInt = "42" // → 42

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • var используется там, где состояние действительно меняется во времени, например при обновлении данных.
  • Ключевые вызовы во фрагменте (getValue, setValue, toInt, IntDelegate) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Делегирование — механизм композиции поведения, интегрированный в модель свойств.


11. Корутины

Корутины — синтаксическая и семантическая конструкция, встроенная в язык через ключевое слово suspend. Они позволяют писать асинхронный код в последовательном стиле, избегая вложенности коллбэков ("callback hell") и сложности управления состоянием вручную.


11.1. suspend-функции — приостанавливаемые вычисления

Функция, помеченная suspend, может приостанавливать своё выполнение без блокировки потока. Это не означает, что она "работает в фоне" — приостановка происходит кооперативно — только в точках, где вызывается другая suspend-функция (например, delay(), withContext(), сетевой запрос).

suspend fun fetchData(): String {
delay(1000) // приостановка на 1 секунду — поток НЕ блокируется
return "result"
}

suspend fun process() {
val data = fetchData() // приостановка до завершения fetchData
println(data)
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Корутины в этом фрагменте показывают неблокирующий стиль: поток не простаивает во время ожиданий.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (fetchData, delay, process, println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

Ключевые принципы:

  • suspend-функцию можно вызывать только из другой suspend-функции или из корутинной области (scope);
  • При компиляции suspend-функция преобразуется в конечный автомат (state machine), где каждая приостановка — переход между состояниями;
  • Нет создания потоков "на лету" — корутины легковесны (одна корутина ≈ несколько десятков байт), и тысячи могут работать параллельно на одном потоке.

11.2. Корутинные области и построители (launch, async, runBlocking)

Для запуска корутин требуется область (CoroutineScope), определяющая жизненный цикл и контекст выполнения.

  • launch — запускает корутину "в фоне", возвращает Job (дескриптор для отмены):
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
val job = scope.launch {
delay(1000)
println("Завершено")
}
job.join() // дождаться завершения
scope.cancel() // отменить область при уничтожении владельца

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Корутины в этом фрагменте показывают неблокирующий стиль: поток не простаивает во время ожиданий.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • join объединяет данные из нескольких таблиц в одном запросе и уменьшает число отдельных походов в БД.
  • Ключевые вызовы во фрагменте (CoroutineScope, SupervisorJob, delay, println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
val deferred = async { fetchData() }
val result = deferred.await() // приостановка до результата

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (fetchData, await) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.
fun main() = runBlocking {
val data = async { fetchData() }
println(data.await())
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (main, fetchData, println, await) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Важно — GlobalScope следует избегать в production-коде — предпочтительны структурированные области (viewModelScope, lifecycleScope в Android, или собственные CoroutineScope), привязанные к жизненному циклу компонента. Это гарантирует, что корутины автоматически отменяются при уничтожении владельца.


11.3. Контексты и диспетчеры

Выполнение корутины происходит в контексте (CoroutineContext), который включает:

  • CoroutineDispatcher — определяет, на каком потоке выполняется блок:
    • Dispatchers.Main — UI-поток (Android, Compose);
    • Dispatchers.Default — пул потоков для CPU-нагрузки;
    • Dispatchers.IO — пул потоков для блокирующих операций (файлы, сеть);
    • Dispatchers.Unconfined — продолжает в том же потоке, где произошла приостановка.

Смена контекста — через withContext():

suspend fun loadAndProcess(): String = withContext(Dispatchers.IO) {
val data = fetchDataFromNetwork() // блокирующий вызов — OK в IO
data.uppercase()
} // возврат в исходный контекст

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Корутины в этом фрагменте показывают неблокирующий стиль: поток не простаивает во время ожиданий.
  • Ключевые вызовы во фрагменте (loadAndProcess, withContext, fetchDataFromNetwork, uppercase) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Это безопаснее, чем async(Dispatchers.IO) { ... }.await(), так как withContext() сохраняет иерархию отмены.


11.4. Flow — реактивные потоки данных

Для работы с последовательностями асинхронных значений используется Flow<T> — холодный, не имеющий состояния, отменяемый аналог RxJava Observable, но с синтаксисом, встроенным в язык.

fun numbers(): Flow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i) // отправка значения
}
}

numbers()
.map { it * 2 }
.filter { it > 3 }
.collect { println(it) } // 4, 6

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Циклическая конструкция в примере показывает повторяемую обработку элементов или шагов алгоритма.
  • Корутины в этом фрагменте показывают неблокирующий стиль: поток не простаивает во время ожиданий.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Ключевые вызовы во фрагменте (numbers, delay, emit, println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

Особенности:

  • flow { ... } — builder для создания Flow;
  • Операторы (map, filter, flatMapConcat, retry, catch) — расширения;
  • collect — терминальная операция, запускающая поток;
  • StateFlow и SharedFlow — горячие потоки для UI-состояний и событий.

Flow интегрируется с suspend-функциями: любой оператор может содержать приостановки, и отмена распространяется корректно.

Корутины не скрывают асинхронность — они структурируют её, делая явной и управляемой, без искажения логики программы.


12. Sealed-классы и when-полноценность

Sealed-классы — это обобщение enum, позволяющее описывать иерархию типов с конечным числом подтипов, известных на этапе компиляции. Они используются для моделирования алгебраических типов данных (ADT), особенно сумм ("или"-типов).

sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • data class автоматически генерирует методы сравнения и копирования, поэтому модель удобна для DTO и сериализации.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (Success, Result, Error) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Особенности:

  • Все прямые подтипы должны быть объявлены в том же файле;
  • Подтипы могут быть class, object, data class;
  • Наследование от sealed-класса закрыто — нельзя расширить его извне.

Главная ценность — полный when без else:

fun handleResult(result: Result) = when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Ошибка: ${result.message}")
Result.Loading -> println("Загрузка...")
// else не требуется — компилятор знает все случаи
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • when группирует ветвление в одном выражении и делает проверку сценариев более прозрачной.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Ключевые вызовы во фрагменте (handleResult, println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

Если добавить новый подтип (data class Timeout — Result()), компилятор укажет на все неполные when — это гарантирует, что изменение модели состояния не приведёт к неперехваченным веткам.

Sealed-классы применяются:

  • Для представления состояний экрана (Idle, Loading, Content, Error);
  • Для обработки результатов API-вызовов;
  • В парсерах, интерпретаторах, конечных автоматах.

Это синтаксическая поддержка defensive programming через систему типов.


13. Объектные выражения и companion-объекты

В Kotlin отсутствует ключевое слово static. Вместо этого используются:


13.1. object-выражения и object-декларации

  • Объектное выражение создаёт анонимный класс с единственным экземпляром — аналог new Runnable() { ... } в Java:
val comparator = object : Comparator<String> {
override fun compare(a: String, b: String) = a.length - b.length
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (compare) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
object Constants {
const val PI = 3.14159
val DATABASE_URL = "jdbc:..."
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.

  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.

  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.

  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

    Обращение: Constants.PI. Экземпляр создаётся при первом обращении (lazy, thread-safe).


13.2. companion object — "почти статика"

Класс может содержать companion object — объект, привязанный к классу и доступный через имя класса:

class User private constructor(val id: Int) {
companion object {
fun fromId(id: Int): User = User(id)
const val MAX_ID = 1000
}
}

val user = User.fromId(42) // вызов "статического" метода

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (constructor, fromId, User) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Особенности:

  • companion object может реализовывать интерфейсы;
  • Члены с const или @JvmField компилируются в static final поля на JVM;
  • Для совместимости с Java используются аннотации:
companion object {
@JvmStatic fun create() = User()
@JvmField val VERSION = "1.0"
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.

  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.

  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.

  • Ключевые вызовы во фрагменте (create, User) формируют основной поток выполнения и обмена данными.

  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

    Тогда User.create() и User.VERSION работают как обычные static-члены.

Это осознанный дизайн: singleton-экземпляр с интерфейсом, а не глобальное состояние без типа.


14. Взаимодействие с Java

Kotlin полностью совместим с Java бинарно и исходно. Однако различия в синтаксисе требуют механизмов сглаживания:


14.1. Платформенные типы (T!)

При вызове Java-кода компилятор Kotlin сталкивается с неизвестной null-семантикой. Результат — платформенный тип (String!), который может использоваться как String или String?. Это временный тип, разрешаемый при присвоении:

val s: String = javaMethod() // OK, если javaMethod() != null во время выполнения
val s: String? = javaMethod() // всегда безопасно

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (javaMethod) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Для точного управления используются аннотации Java (@Nullable, @NonNull), которые Kotlin учитывает.


14.2. Аннотации для совместимости

  • @JvmStatic — метод в companion object становится static;
  • @JvmOverloads — генерирует перегрузки для параметров по умолчанию;
  • @JvmName — задаёт имя метода на JVM (полезно при конфликтах сигнатур);
  • @Throws — указывает проверяемые исключения (для совместимости с Java-контрактом);
  • @JvmField — делает свойство полем без геттера/сеттера.

Пример:

class StringUtils {
@JvmStatic
@JvmOverloads
fun repeat(str: String, times: Int = 3): String = str.repeat(times)
}

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Ключевые вызовы во фрагменте (repeat) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

В Java: StringUtils.repeat("a"), StringUtils.repeat("a", 5).


14.3. Обработка проверяемых исключений

Kotlin не требует объявления throws и не проверяет исключения на этапе компиляции. Но при вызове Java-методов, бросающих IOException, его можно перехватить как обычно:

try {
Files.readAllBytes(path)
} catch (e: IOException) {
// обработка
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • Ключевые вызовы во фрагменте (readAllBytes, catch) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Без @Throws Kotlin считает, что метод не бросает проверяемых исключений — это упрощает написание, но требует осторожности при интеграции.


15. Kotlin Multiplatform — expect/actual и общая логика

Kotlin Multiplatform (KMP) — архитектурная парадигма, поддерживаемая компилятором через синтаксические конструкции для разделения общего и платформенно-специфичного кода.


15.1. expect и actual — договор о реализации

В общем модуле (commonMain) объявляется ожидаемая сущность через expect:

expect class PlatformLogger() {
fun log(message: String)
}

expect fun getPlatformName(): String

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Ключевые вызовы во фрагменте (PlatformLogger, log, getPlatformName) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

В платформенных модулях (androidMain, iosMain, jvmMain) — её реализация через actual:

// androidMain
actual class PlatformLogger actual constructor() {
actual fun log(message: String) {
android.util.Log.d("APP", message)
}
}

actual fun getPlatformName() = "Android"

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Ключевые вызовы во фрагменте (constructor, log, d, getPlatformName) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.
// iosMain
actual class PlatformLogger actual constructor() {
actual fun log(message: String) {
NSLog(message)
}
}

actual fun getPlatformName() = "iOS"

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Ключевые вызовы во фрагменте (constructor, log, NSLog, getPlatformName) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Особенности:

  • expect может применяться к классам, функциям, свойствам, аннотациям;
  • actual-реализация должна точно соответствовать сигнатуре expect (включая модификаторы open, suspend);
  • actual может использовать платформенные API (Android SDK, iOS Foundation), недоступные в common.

Это статическая композиция: на этапе линковки выбирается нужная реализация. Ошибки несоответствия обнаруживаются на этапе компиляции.


15.2. Ограничения и обходные пути

  • В common нельзя использовать платформенные типы напрямую → решения:

    • Абстракция через интерфейсы + expect-фабрики;
    • @OptionalExpectation — пометка, что реализация может отсутствовать;
    • expect object для singleton-сервисов.
  • actual не может расширять expect class → вместо этого используется композиция.

  • Для общих DTO используется @Serializable (см. ниже) или простые data class в common.

KMP позволяет выносить до 80–90% бизнес-логики в общий код, оставляя платформенный слой тонким и тестируемым.


16. Kotlin/JS — синтаксис для мира JavaScript

При компиляции в JavaScript синтаксис Kotlin адаптируется под экосистему JS, сохраняя семантику, но добавляя механизмы взаимодействия.


16.1. Внешние объявления (external)

Для использования существующих JS-библиотек объявляются external сущности:

external fun fetch(url: String): Promise<Response>

external class Response {
fun json(): Promise<dynamic>
}

external val window: Window

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (fetch, json) формируют основной поток выполнения и обмена данными.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

external — это "доверенное" объявление: компилятор предполагает, что символ существует во время выполнения. Ошибки проявятся только в runtime — поэтому часто используются вместе с инструментами вроде kotlinx.js или генераторами из .d.ts.


16.2. dynamic — обход системы типов для совместимости

Тип dynamic отключает проверку типов во время компиляции:

val obj: dynamic = getObjectFromJS()
obj.method("arg") // любой вызов разрешён

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (getObjectFromJS, method) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Это необходимо при работе с динамическими API (например, eval, JSON.parse), но его использование должно быть минимизировано — предпочтительны external интерфейсы.


16.3. Модульность и экспорт

  • @JsExport — делает класс/функцию доступной из JS:
@JsExport
class Calculator {
fun add(a: Double, b: Double) = a + b
}

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.

  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.

  • Ключевые вызовы во фрагменте (add) формируют основной поток выполнения и обмена данными.

  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

    В JS: new Calculator().add(2, 3).

  • @JsName — задаёт имя в JS (полезно при перегрузке или зарезервированных словах):

@JsName("createUser")
fun createUser(name: String) = User(name)

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Ключевые вызовы во фрагменте (JsName, createUser, User) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.
@JsModule("axios")
external val axios: Axios

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (JsModule) формируют основной поток выполнения и обмена данными.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Kotlin/JS сохраняет статическую типизацию там, где это возможно, и чётко локализует "дыры" в типобезопасности через external и dynamic.


17. Kotlin/Native

Kotlin/Native компилируется в машинный код (через LLVM) и работает без виртуальной машины. Его синтаксис учитывает особенности управления памятью и межпоточности.


17.1. Управление памятью

Kotlin/Native использует подсчёт ссылок с циклическим сборщиком (ARC + cycle collector), а не stop-the-world GC. Это накладывает ограничения:

  • Объекты "замораживаются" (freeze()) при передаче между потоками;
  • Заморожённые объекты становятся неизменяемыми и разделяемыми;
  • @SharedImmutable — аннотация для констант, безопасных для совместного доступа:
@SharedImmutable
val PI = 3.14159

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Фрагмент полезно читать сверху вниз: каждая строка подготавливает контекст для следующего шага.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Синтаксически это проявляется в необходимости явного управления жизненным циклом в многопоточных сценариях — но компилятор помогает: попытка изменить замороженный объект вызовет InvalidMutabilityException.


17.2. C-интероперабельность

Для вызова C-кода используются extern-объявления (через cinterop):

fun main() {
memScoped {
val buffer = allocArray<ByteVar>(256)
fgets(buffer, 256, stdin)
val str = buffer.toKString()
println("Вы ввели: $str")
}
}

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • println используется как быстрая проверка результата и помогает увидеть фактический ход выполнения.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Ключевые вызовы во фрагменте (main, fgets, toKString, println) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.

Генерируются Kotlin-обёртки над заголовочными файлами, где:

  • IntVar, ByteVar — указатели на примитивы;
  • CPointer<T> — безопасная абстракция над void*;
  • memScoped — управляет временем жизни стека.

Это не "unsafe-код" в стиле Rust — границы безопасности сохраняются через области (scope), но с явным указанием зон ответственности.


18. DSL-конструирование

Kotlin предоставляет несколько механизмов для создания внутренних DSL — API, читающихся как специализированный язык:


18.1. Infix-функции

Позволяют вызывать бинарные функции без точек и скобок:

infix fun Int.times(str: String) = str.repeat(this)

val result = 3 times "Hello " // → "Hello Hello Hello "

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (times, repeat) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Используется в тестовых фреймворках (1 shouldBe 1), маршрутизации (get "/users" { ... }).


18.2. Операторные функции

Перегрузка операторов (+, -, [], invoke, ..) через operator fun:

operator fun User.plus(other: User) = User(this.id + other.id, "${this.name}, ${other.name}")

val combined = user1 + user2

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Интерполяция ${...} или $name в строках делает вывод компактным и избавляет от ручной конкатенации.
  • Ключевые вызовы во фрагменте (plus, User) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Важно — перегрузка должна сохранять семантику оператора (например, + — коммутативен), иначе читаемость страдает.


18.3. @DslMarker — защита от неправильного вложения

Аннотация предотвращает случайный выход из контекста DSL:

@DslMarker
annotation class HtmlTagMarker

@HtmlTagMarker
abstract class Tag {
abstract fun render(): String
}

class Body : Tag() {
override fun render() = "<body>...</body>"
}

// Внутри HtmlDsl нельзя вызывать методы вне текущего тега

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Ключевые вызовы во фрагменте (render, Tag) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Это гарантирует, что в html { body { div { ... } } } нельзя "выпрыгнуть" из div в html напрямую — повышает безопасность DSL.


19. Аннотации как расширения языка

Аннотации в Kotlin — не просто метаданные. Они активно участвуют в компиляции и позволяют встраивать новые возможности без изменения синтаксиса ядра.


19.1. @Serializable — сериализация как часть типа

@Serializable
data class User(val id: Int, val name: String)

val json = Json.encodeToString(User(1, "Alice"))
val user = Json.decodeFromString<User>(json)

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • data class автоматически генерирует методы сравнения и копирования, поэтому модель удобна для DTO и сериализации.
  • val фиксирует ссылку после инициализации и делает поведение участка более предсказуемым.
  • Ключевые вызовы во фрагменте (User, encodeToString) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Плагин компиляции генерирует сериализатор на этапе компиляции — без reflection, быстро и безопасно. Поддерживает JSON, ProtoBuf, CBOR.


19.2. @OptIn и @RequiresOptIn — контролируемое использование экспериментальных API

Разработчик API может пометить функцию как экспериментальную:

@RequiresOptIn(message = "Эта функция может измениться")
annotation class ExperimentalIo

@ExperimentalIo
suspend fun experimentalRead() = ...

Разбор:

  • Фрагмент строит структуру типов: объявления классов/интерфейсов задают контракт и модель данных.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Корутины в этом фрагменте показывают неблокирующий стиль: поток не простаивает во время ожиданий.
  • Ключевые вызовы во фрагменте (RequiresOptIn, experimentalRead) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Пользователь должен явно согласиться:

@OptIn(ExperimentalIo::class)
suspend fun load() = experimentalRead()

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Корутины в этом фрагменте показывают неблокирующий стиль: поток не простаивает во время ожиданий.
  • Ключевые вызовы во фрагменте (OptIn, load, experimentalRead) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • В практическом коде этот шаблон обычно оборачивают обработкой ошибок и проверкой входных данных.

Это предотвращает случайное использование unstable-кода и даёт понятную миграционную траекторию.


19.3. @Deprecated — эволюция API с контролем

Расширенная версия @Deprecated из Java:

@Deprecated(
message = "Используйте processAsync",
replaceWith = ReplaceWith("processAsync()", "import suspend processAsync"),
level = DeprecationLevel.ERROR
)
fun process() { ... }

Разбор:

  • Фрагмент показывает исполняемый сценарий: здесь важен порядок шагов и состояние между вызовами.
  • fun выделяет именованные блоки логики, что упрощает повторное использование и тестирование кода.
  • Корутины в этом фрагменте показывают неблокирующий стиль: поток не простаивает во время ожиданий.
  • Ключевые вызовы во фрагменте (Deprecated, ReplaceWith, processAsync, process) формируют основной поток выполнения и обмена данными.
  • Операции присваивания связывают вычисленные значения с переменными и делают промежуточные шаги явно читаемыми.
  • Границы блоков в фигурных скобках помогают сразу увидеть области видимости и жизненный цикл локальных переменных.

Компилятор может:

  • Предупреждать (WARNING);
  • Запрещать использование (ERROR);
  • Автоматически предлагать замену (в IDE).

Это инструмент ответственного рефакторинга.

Дополнительные сниппеты

sealed class AuthState {
data object Loading : AuthState()
data class Authorized(val userId: Long) : AuthState()
data class Failed(val reason: String) : AuthState()
}

fun render(state: AuthState): String = when (state) {
AuthState.Loading -> "Проверка доступа..."
is AuthState.Authorized -> "Пользователь: ${state.userId}"
is AuthState.Failed -> "Ошибка: ${state.reason}"
}

Разбор:

  • sealed class фиксирует полный набор состояний, что делает модель явной и конечной.
  • when обрабатывает все варианты AuthState и возвращает строку как значение выражения.
  • Ветви is AuthState.Authorized и is AuthState.Failed одновременно проверяют тип и открывают доступ к полям.
  • Такой шаблон хорошо подходит для UI-состояний и логики, где важна исчерпывающая обработка сценариев.
  • Интерполяция в строках делает итоговый текст читаемым без промежуточных переменных.
inline fun <T> T.log(stage: String): T {
println("[$stage] $this")
return this
}

val result = listOf(1, 2, 3, 4)
.map { it * 2 }
.filter { it > 4 }
.log("after-filter")

Разбор:

  • Extension-функция log добавляет диагностический шаг в цепочку без изменения основной бизнес-логики.
  • Параметризированный тип <T> позволяет использовать один и тот же сниппет для любых значений.
  • Возврат this сохраняет флюентный стиль: после логирования цепочка продолжает работать с тем же объектом.
  • map и filter показывают функциональную обработку коллекции как последовательность чистых преобразований.
  • Этот шаблон удобен для отладки пайплайнов данных в сервисах и обработчиках событий.