Основы Kotlin
Ключевые темы Kotlin, которые важно понимать перед работой с Android-кодом: изменяемость, null-безопасность, корневые типы, сравнение, приведение типов и правила наследования.
Основы языка
val и var
val объявляет ссылку только для чтения: после инициализации ей нельзя присвоить другое значение. var объявляет изменяемую ссылку, которой можно присваивать новые значения.
Важно: val не делает объект по ссылке неизменяемым. Например, val list = mutableListOf(1, 2, 3) запрещает переназначить list, но не мешает изменить содержимое списка через list.add(4).
Предпочитайте val и переходите на var, только когда переназначение действительно необходимо. Это уменьшает количество случайных изменений состояния и упрощает понимание кода.
Nullable-типы
Nullable-тип может хранить null. Возможность null является частью системы типов Kotlin: String не может быть null, а String? может.
Компилятор требует явно обработать nullable-значение. Обычно для этого используют оператор безопасного вызова ?., Elvis-оператор ?:, проверку на null с последующим smart cast или, в редких случаях, оператор утверждения о ненулевом значении !!.
Оператор !! отключает эту защиту компилятора и может привести к NullPointerException. Избегайте его, если ненулевой инвариант не гарантирован и его можно выразить безопаснее.
Ещё одна важная граница - взаимодействие с Java. Объявления Java с неизвестной nullability становятся платформенными типами, поэтому Kotlin может разрешить небезопасное обращение, которое завершится ошибкой во время выполнения. Учитывайте аннотации nullability и задокументированные контракты значений из Java- и Android-API.
Any / Unit / Nothing
Any - общий супертип всех ненулевых типов Kotlin, похожий на Java Object. Он предоставляет equals(), hashCode() и toString(). Если нужно также допустить null, используется Any?.
Unit - тип результата функции, которая не возвращает полезного значения. Он похож на Java void, но является настоящим типом с единственным значением Unit. Обычно его не указывают явно.
У типа Nothing нет значений. Он представляет выражение, которое никогда не завершается нормально, например всегда выбрасывает исключение или выполняет бесконечный цикл. Поскольку Nothing является подтипом любого типа Kotlin, вызов error() можно использовать там, где ожидается результат любого типа.
fun log(message: String) {
println(message)
}
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
== и ===
== проверяет структурное равенство через equals(). Выражение a == b примерно эквивалентно a?.equals(b) ?: (b == null).
=== проверяет ссылочное равенство: указывают ли две ссылки на один и тот же объект.
У data class сгенерированный equals() сравнивает свойства, объявленные в первичном конструкторе. Важное исключение - массивы: их реализация equals() сравнивает идентичность, поэтому для содержимого нужны contentEquals() или contentDeepEquals().
data class User(val id: Long)
val first = User(1)
val second = User(1)
println(first == second) // true
println(first === second) // false
=== нужен значительно реже - обычно когда важна сама идентичность объекта, например при проверке, что две ссылки ведут на один экземпляр из кеша.
as и as?
as выполняет небезопасное приведение. Если значение имеет несовместимый тип, возникает ClassCastException. Приведение null к ненулевому типу также завершается ошибкой во время выполнения.
as? выполняет безопасное приведение. Он возвращает значение требуемого типа или null, если приведение невозможно.
Явное приведение часто не требуется. После подходящей проверки через is компилятор может выполнить smart cast для стабильного значения:
open / final по умолчанию
В Java классы и методы можно наследовать или переопределять, если они не отмечены как final. В Kotlin используется обратный подход: классы и переопределяемые члены final по умолчанию.
Чтобы разрешить наследование или переопределение, класс либо его член нужно отметить как open. Для переопределения используется override, а final override запрещает дальнейшее переопределение.
open class BaseRepository {
open fun load() = "data"
}
class UserRepository : BaseRepository() {
final override fun load() = "users"
}
Такой подход делает точки расширения явными и снижает риск случайно изменить унаследованное поведение.