Введение: Почему даже умные разработчики спотыкаются на 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