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

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

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

Начать курс

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

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

1. Неправильное использование корутин: утечки памяти и потеря контекста

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

Ошибка: глобальный scope в Activity

// ПЛОХО: корутина живёт дольше Activity
class MyActivity : AppCompatActivity() {
    fun loadData() {
        GlobalScope.launch {
            val data = fetchFromNetwork()
            withContext(Dispatchers.Main) {
                textView.text = data // Утечка, если Activity уже уничтожена
            }
        }
    }
}

Решение: использование lifecycleScope или viewModelScope

// ХОРОШО: корутина отменяется при уничтожении Activity
class MyActivity : AppCompatActivity() {
    fun loadData() {
        lifecycleScope.launch {
            val data = withContext(Dispatchers.IO) { fetchFromNetwork() }
            textView.text = data
        }
    }
}

Совет: Всегда используйте CoroutineScope, привязанный к жизненному циклу. Для ViewModel — viewModelScope, для Activity/Fragment — lifecycleScope. Никогда не используйте GlobalScope в продакшене.

2. Злоупотребление null safety: неявные допущения

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

Ошибка: игнорирование nullable типов

// ПЛОХО: может выбросить NullPointerException
fun processUser(user: User?) {
    val name = user!!.name
    println(name)
}

Решение: использование элвис-оператора или let

// ХОРОШО: безопасное извлечение
fun processUser(user: User?) {
    val name = user?.name ?: "Unknown"
    println(name)
    
    // Или с let:
    user?.let { u ->
        println(u.name)
    }
}

Важно: Оператор !! оправдан только в тестах или когда вы на 100% уверены, что значение не null (например, после проверки в if). В остальных случаях используйте ?.let, ?: или requireNotNull().

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

Sealed классы — отличный способ моделировать ограниченные иерархии. Однако начинающие часто забывают про when как выражение и не учитывают все ветки.

Ошибка: пропуск веток в when

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

// ПЛОХО: компилятор не предупредит об отсутствии Loading
fun handleResult(result: Result) {
    when (result) {
        is Result.Success -> println(result.data)
        is Result.Error -> println(result.message)
        // Loading забыт!
    }
}

Решение: использование when как выражения

// ХОРОШО: компилятор заставит обработать все ветки
fun handleResult(result: Result): String {
    return when (result) {
        is Result.Success -> result.data
        is Result.Error -> result.message
        is Result.Loading -> "Загрузка..."
    }
}

Совет: Всегда используйте when как выражение (с возвратом значения) для sealed классов. Если нужна только обработка, добавьте else или явно укажите все подтипы.

4. Ошибки при работе с inline функциями и reified типами

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

Ошибка: inline для больших функций

// ПЛОХО: тело функции копируется в каждое место вызова
inline fun heavyOperation(block: () -> Unit) {
    // Много кода...
    block()
    // Ещё много кода...
}

Решение: inline только для маленьких функций

// ХОРОШО: inline для коротких функций-обёрток
inline fun measureTime(block: () -> Unit) {
    val start = System.nanoTime()
    block()
    println("Время: ${System.nanoTime() - start} нс")
}

// Для больших функций — обычная функция
fun heavyOperation(block: () -> Unit) {
    // Много кода...
    block()
}

Ошибка: reified без необходимости

// ПЛОХО: reified тип, но можно обойтись без inline
inline fun  createInstance(): T {
    return T::class.java.newInstance()
}

Совет: Используйте inline только когда это действительно нужно: для reified типов, передачи лямбд с non-local return или для маленьких утилит. Для больших функций это ухудшит производительность из-за раздувания кода.

5. Делегированные свойства: путаница с lazy и lateinit

Многие путают lazy и lateinit, что приводит к неожиданным исключениям.

Ошибка: lateinit для nullable типов

// ПЛОХО: lateinit не работает с nullable
lateinit var name: String? // Ошибка компиляции!

Решение: правильный выбор делегата

// ХОРОШО: lazy для ленивой инициализации
val name: String by lazy {
    computeExpensiveValue()
}

// ХОРОШО: lateinit для обязательных полей (только non-null)
lateinit var name: String

fun init() {
    name = "Kotlin"
}

Блоги

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