Топ ошибок начинающих 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"
}
Блоги
Рекомендаций по книгам
Читайте также
Работа с JSON в Swift: парсинг и сериализация данных
Размер экрана, окна и страницы в JavaScript: 5 фатальных ошибок и как их избежать
Создаем объект по интерфейсу TypeScript: полный гайд с примерами
WebSocket на C#: real-time приложения продвинутого уровня
Code Review в Go: что проверять и как давать обраттную связь на практике