Синтаксические конструкции 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 = 30 → Int). Это не "динамическая типизация" — тип фиксируется на этапе компиляции, и попытка присвоить 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показывают функциональную обработку коллекции как последовательность чистых преобразований.- Этот шаблон удобен для отладки пайплайнов данных в сервисах и обработчиках событий.