Топ ошибок начинающих Kotlin разработчиков: как их избежать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Топ ошибок начинающих Kotlin разработчиков: как их избежать и писать чистый код

Kotlin — современный, лаконичный и безопасный язык, который быстро завоевал популярность в мире Android-разработки и бэкенда. Однако его гибкость и синтаксический сахар часто вводят новичков в заблуждение. В этой статье мы разберём 4 типичные ошибки, которые совершают начинающие разработчики на Kotlin, и покажем, как их исправить с помощью примеров кода.

1. Игнорирование null safety и неправильное использование оператора !!

Одна из главных фишек Kotlin — null safety. Но многие новички, привыкшие к Java, продолжают использовать !! (not-null assertion operator) бездумно. Это прямой путь к NullPointerException.

Плохой пример:

fun getUserName(user: User?): String {
    return user!!.name // Если user == null, будет NPE
}

Хороший пример (безопасная работа с nullable):

fun getUserNameSafe(user: User?): String {
    return user?.name ?: "Неизвестный пользователь"
}

Используйте ?.let{}, ?: (элвис-оператор) и ?.run{} для безопасного доступа. Оператор !! оправдан только в редких случаях, когда вы на 100% уверены, что значение не null (например, в тестах).

2. Неправильное использование sealed классов и when

Sealed классы — мощный инструмент для моделирования ограниченных иерархий. Но начинающие часто забывают, что when должен быть исчерпывающим (exhaustive) при работе с sealed классами. Или, наоборот, используют else там, где он не нужен.

Плохой пример (пропущена ветка):

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

fun handleResult(result: Result) {
    when (result) {
        is Result.Success -> println(result.data)
        // Ветка для Error отсутствует — будет предупреждение
    }
}

Хороший пример (исчерпывающий when):

fun handleResultExhaustive(result: Result) {
    when (result) {
        is Result.Success -> println(result.data)
        is Result.Error -> println(result.message)
    }
    // Компилятор не требует else, так как все варианты покрыты
}

Если вы добавляете новую ветку в sealed класс, компилятор подскажет, где нужно обновить when. Это защищает код от ошибок во время рефакторинга.

3. Злоупотребление корутинами: запуск без контекста и игнорирование structured concurrency

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

Плохой пример (глобальный scope и потеря контекста):

fun loadData() {
    GlobalScope.launch { // Плохо! Живёт, пока живёт приложение
        val data = fetchData() // Может выполняться после закрытия Activity
        updateUI(data) // Ошибка: UI уже уничтожен
    }
}

Хороший пример (structured concurrency с viewModelScope):

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            val data = fetchData()
            updateUI(data) // Автоматически отменится при очистке ViewModel
        }
    }
}

Всегда используйте coroutineScope, viewModelScope или свой собственный CoroutineScope с чётким жизненным циклом. Избегайте GlobalScope в production-коде.

4. Перегрузка операторов без понимания семантики

Kotlin позволяет перегружать операторы (+, -, in и др.) для своих классов. Это удобно, но новички часто злоупотребляют этой возможностью, делая код нечитаемым и неинтуитивным.

Плохой пример (неестественная перегрузка):

data class Vector(val x: Int, val y: Int) {
    operator fun plus(other: Vector): Vector {
        return Vector(x + other.x, y + other.y)
    }
    // Сложение векторов — логично. А что насчёт умножения?
    operator fun times(other: Vector): Vector {
        return Vector(x * other.x, y * other.y) // Покомпонентное умножение? Или скалярное?
    }
}

Проблема: умножение векторов может быть интерпретировано по-разному (скалярное, векторное, покомпонентное). Лучше использовать именованные функции:

data class Vector(val x: Int, val y: Int) {
    operator fun plus(other: Vector) = Vector(x + other.x, y + other.y)
    fun dot(other: Vector): Int = x * other.x + y * other.y
    fun cross(other: Vector): Vector = Vector(x * other.x, y * other.y) // Или другой смысл
}

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

Заключение

Kotlin — язык, который прощает многие ошибки, но не все. Чтобы писать надёжный и поддерживаемый код, помните:

  • Избегайте !! — используйте безопасные вызовы и элвис-оператор.
  • Делайте when исчерпывающим для sealed классов.
  • Всегда привязывайте корутины к жизненному циклу (structured concurrency).
  • Перегружайте операторы только с ясной семантикой.

Освоив эти принципы, вы не только избавитесь от типичных ошибок, но и сделаете свой код более чистым и понятным. Удачи в изучении Kotlin!

Блоги

Рекомендаций по книгам