Топ ошибок начинающих Kotlin разработчиков: от А до Я

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

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

Начать курс

Введение: Почему даже умные разработчики спотыкаются на Kotlin

Kotlin — элегантный и мощный язык, который завоевал сердца миллионов разработчиков. Но его лаконичность и гибкость таят в себе ловушки, особенно для новичков, пришедших из Java или других языков. Многие считают, что раз Kotlin «понятнее» Java, то и ошибок будет меньше. На практике всё наоборот: непонимание философии языка приводит к багам, которые трудно отловить.

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

1. Null Safety: когда «?» спасает, а когда — нет

1.1. Игнорирование nullable типов

Самая распространённая ошибка — использовать !! (not-null assertion) там, где можно обойтись безопасными вызовами. Новички часто пишут:

fun printLength(str: String?) {
    println(str!!.length) // Бросит NPE, если str == null
}

Правильный подход — использовать ?.let или элвис-оператор ?::

fun printLength(str: String?) {
    str?.let { println(it.length) } ?: println("Строка null")
}

1.2. Забывание про Smart Cast

Kotlin автоматически приводит типы после проверки на null, но новички этого не замечают:

fun process(value: Any?) {
    if (value is String) {
        // value уже String, не нужно кастовать!
        println(value.length) // Работает без (value as String).length
    }
}

Совет: Доверяйте компилятору. Используйте !! только если вы на 100% уверены, что значение не null, и готовы к крашу при ошибке.

2. Sealed Class и When: подводные камни исчерпываемости

2.1. Использование else-ветки с sealed class

Sealed class созданы для того, чтобы компилятор проверял, все ли варианты обработаны. Но новички часто добавляют 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)
        is Result.Error -> println(result.message)
        else -> {} // Ошибка! Если добавить новый подкласс, компилятор не предупредит
    }
}

Правильно — не использовать else:

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

2.2. Путаница между sealed class и enum

Новички часто выбирают enum там, где нужен sealed class. Запомните: enum — для конечного набора констант, sealed class — для иерархии с разными данными в каждом подклассе.

3. Scope Functions: apply, let, run, with, also — как не запутаться

3.1. Смешивание контекста: this vs it

Самая частая путаница — когда новичок использует apply (контекст — this) для операций, где нужен let (контекст — it):

data class User(var name: String, var age: Int)

fun initUser(): User {
    return User("Аноним", 0).apply {
        // this — это User
        name = "Иван"
        age = 25
    }
}

fun printUserInfo(user: User?) {
    user?.let { 
        // it — это user
        println("Имя: ${it.name}, Возраст: ${it.age}")
    }
}

3.2. Цепочки scope functions без смысла

Новички любят писать цепочки вроде:

list?.let { it.filter { it > 5 }.also { println(it) } }

Это работает, но читается тяжело. Лучше разбить на отдельные переменные или использовать run только когда это действительно упрощает код.

3.3. Забывание про возвращаемое значение

  • apply и also возвращают исходный объект
  • let, run, with возвращают результат последнего выражения

Пример ошибки: попытка сохранить результат apply как новое значение:

val user = User("Анна", 30).apply {
    name = "Мария"
}
// user.name == "Мария", user.age == 30 — apply изменил исходный объект

4. Coroutines: гонка за производительностью с багами

4.1. Запуск корутин без GlobalScope

Новички часто используют GlobalScope.launch, что приводит к утечкам памяти:

GlobalScope.launch {
    // Корутина живёт, пока живёт приложение
    delay(1000)
    // Потенциальная утечка, если активити уже уничтожена
}

Правильно — использовать CoroutineScope с привязкой к жизненному циклу (в Android — lifecycleScope или viewModelScope):

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

4.2. Неправильное использование withContext

Путаница между Dispatchers.IO и Dispatchers.Default. Dispatchers.IO — для блокирующих операций (чтение файлов, сетевые запросы), Dispatchers.Default — для CPU-интенсивных задач:

// Неправильно: CPU-задача на IO
withContext(Dispatchers.IO) {
    calculateFibonacci(40) // Блокирует пул IO
}

// Правильно: CPU-задача на Default
withContext(Dispatchers.Default) {
    calculateFibonacci(40                

Блоги

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