16 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Свойства | Геттеры и сеттеры, backing field, вычисляемые свойства, lateinit, lazy и делегаты |
|
Свойство - это поле плюс методы доступа. Снаружи выглядит как поле, но обращение всегда идёт через геттер и сеттер.
class User {
val id: Int = 1 // только чтение: геттер
var name: String = "Иван" // чтение и запись: геттер + сеттер
}
fun main() {
val u = User()
println(u.id) // вызов геттера
u.name = "Пётр" // вызов сеттера
println(u.name) // Пётр
}
Полная форма объявления:
class Rect {
var width: Int = 0
get() = field
set(value) { field = value }
}
fun main() {
val r = Rect()
r.width = 5
println(r.width) // 5
}
Такой код писать не нужно - это то, что компилятор генерирует сам. Писать вручную стоит только нестандартное поведение.
field - backing field
Внутри геттера и сеттера доступен field - само хранилище. Прямое обращение к свойству по имени привело бы к бесконечной рекурсии.
class Counter {
var value: Int = 0
set(newValue) {
println("было $field, стало $newValue")
field = newValue // так
// value = newValue // бесконечная рекурсия!
}
}
fun main() {
val c = Counter()
c.value = 5 // было 0, стало 5
c.value = 7 // было 5, стало 7
println(c.value) // 7
}
field появляется только если он реально нужен. У вычисляемого свойства хранилища нет:
class Rect2(val width: Int, val height: Int) {
val area: Int get() = width * height // поля нет, считается каждый раз
}
fun main() {
println(Rect2(2, 3).area) // 6
}
Это важно для памяти: свойство-геттер не занимает места в объекте.
Валидация в сеттере
class Account {
var balance: Long = 0
set(value) {
require(value >= 0) { "баланс не может быть отрицательным: $value" }
field = value
}
}
fun main() {
val a = Account()
a.balance = 100
println(a.balance) // 100
try {
a.balance = -1
} catch (e: IllegalArgumentException) {
println(e.message) // баланс не может быть отрицательным: -1
}
}
Видимость сеттера
Частый приём - читать можно всем, менять только внутри:
class Session {
var token: String = ""
private set // геттер public, сеттер private
fun login(t: String) { token = t }
}
fun main() {
val s = Session()
s.login("abc")
println(s.token) // abc
// s.token = "x" // ошибка: сеттер приватный
}
Это аккуратнее, чем приватное поле плюс отдельный публичный геттер.
Вычисляемые свойства или функции
class Order3(val items: List<Int>) {
val total: Int get() = items.sum() // свойство
fun computeTotal() = items.sum() // функция
}
fun main() {
val o = Order3(listOf(1, 2, 3))
println("${o.total} ${o.computeTotal()}") // 6 6
}
Соглашение: свойство, если это характеристика объекта, вычисляется дешёво и не бросает исключений. Функция - если выполняется работа: запрос, вычисление сложнее O(1), обращение к сети.
class Data2(val rows: List<String>) {
val size: Int get() = rows.size // дешёво — свойство
fun parse(): List<Int> = rows.map { it.length } // работа — функция
}
fun main() {
val d = Data2(listOf("ab", "cde"))
println("${d.size} ${d.parse()}") // 2 [2, 3]
}
Инициализация и порядок
Свойство должно быть инициализировано при создании объекта:
class Service5 {
val a: Int = 1 // сразу
val b: Int // в init
init { b = a + 1 }
val c: Int get() = a + b // геттер, инициализации не требует
}
fun main() {
val s = Service5()
println("${s.a} ${s.b} ${s.c}") // 1 2 3
}
Подробнее: Инициализация
lateinit
Когда значение появится позже, но хочется non-null тип:
class Service6 {
lateinit var client: String
fun setup() { client = "готов" }
fun use() = if (::client.isInitialized) client else "не настроен"
}
fun main() {
val s = Service6()
println(s.use()) // не настроен
s.setup()
println(s.use()) // готов
println(s.client) // готов
}
Ограничения lateinit:
- только
var - только non-null ссылочный тип: для
Int,Doubleи других примитивов нельзя - нельзя задать свой геттер или сеттер
- проверка
::x.isInitializedдоступна только внутри класса
Обращение до инициализации даёт понятную ошибку:
class Service7 { lateinit var name: String }
fun main() {
try {
println(Service7().name)
} catch (e: UninitializedPropertyAccessException) {
println(e.message) // lateinit property name has not been initialized
}
}
lateinit нужен для внедрения зависимостей и жизненных циклов фреймворков (Android, Spring). В остальных случаях лучше nullable-тип или lazy.
lazy
Вычисляется один раз при первом обращении:
class Config5 {
val heavy: String by lazy {
println("вычисляем...")
"результат"
}
}
fun main() {
val c = Config5()
println("создан") // создан
println(c.heavy) // вычисляем... затем результат
println(c.heavy) // результат — повторно не считается
}
lazy работает только с val. По умолчанию потокобезопасен (синхронизация при первом доступе); если объект гарантированно в одном потоке, можно ослабить:
val value: Int by lazy(LazyThreadSafetyMode.NONE) { 42 }
fun main() { println(value) } // 42
lateinit var |
by lazy |
|
|---|---|---|
| изменяемость | var |
val |
| кто задаёт значение | внешний код | сама лямбда |
| примитивы | нельзя | можно |
| потокобезопасность | нет | есть по умолчанию |
| доступ до инициализации | исключение | вычислит на месте |
observable и vetoable
Готовые делегаты для реакции на изменение:
import kotlin.properties.Delegates
class Form {
var name: String by Delegates.observable("") { _, old, new ->
println("name: '$old' -> '$new'")
}
var age: Int by Delegates.vetoable(0) { _, _, new ->
new in 0..150 // false отменяет присваивание
}
}
fun main() {
val f = Form()
f.name = "Иван" // name: '' -> 'Иван'
f.age = 20
f.age = 500 // отклонено
println(f.age) // 20
}
Delegates.notNull() - альтернатива lateinit для примитивов:
import kotlin.properties.Delegates
class Service8 {
var port: Int by Delegates.notNull()
}
fun main() {
val s = Service8()
s.port = 8080
println(s.port) // 8080
}
Свойства в Map
Делегирование в Map - удобно для разбора динамических данных:
class User2(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
fun main() {
val u = User2(mapOf("name" to "Иван", "age" to 20))
println("${u.name} ${u.age}") // Иван 20
}
Свой делегат
import kotlin.reflect.KProperty
class Trimmed {
private var value = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
value = newValue.trim()
}
}
class Form2 {
var login: String by Trimmed()
}
fun main() {
val f = Form2()
f.login = " иван "
println("'${f.login}'") // 'иван'
}
Подробнее: Делегирование
Свойства верхнего уровня и расширения
val appName = "MyApp" // свойство файла
const val VERSION = "1.0" // константа компиляции
val String.firstWord: String // свойство-расширение
get() = substringBefore(" ")
fun main() {
println("$appName $VERSION") // MyApp 1.0
println("привет мир".firstWord) // привет
}
У свойства-расширения не может быть field - только геттер. Подробнее: Функции-расширения
[!info] Отличия от Java
Java Kotlin поле + getX()+setX()var xobj.getName()obj.nameobj.setName("x")obj.name = "x"private finalполеvalгеттер с вычислением val x get() = ...Lombok @Getter/@Setterне нужны Supplier+ кеш для ленивостиby lazyOptional<T>полеT?Свойства - не синтаксический сахар над геттерами, а единица абстракции. Переход от хранимого свойства к вычисляемому не меняет код клиентов:
class Rect3(val w: Int, val h: Int) { val area: Int = w * h // было: хранится } class Rect4(val w: Int, val h: Int) { val area: Int get() = w * h // стало: считается. Клиенты не изменились } fun main() { println("${Rect3(2,3).area} ${Rect4(2,3).area}") } // 6 6В Java переход от
obj.fieldкobj.getField()- ломающее изменение, поэтому там поля сразу прячут за геттерами «на всякий случай». В Kotlin эта защита встроена.Java-геттеры видны как свойства. Метод
getName()в Java-классе доступен из Kotlin какobj.name:fun main() { val sb = StringBuilder("abc") println(sb.length) // вместо sb.length() — синтетическое свойство }
by lazyзаменяет двойную проверку блокировки. Классический ленивый синглтон в Java требуетvolatileи синхронизации; в Kotlin это одна строка.Сравнить: Инкапсуляция в Java
[!warning] Подводные камни Обращение к свойству внутри его сеттера - бесконечная рекурсия:
class Bad2 { var x: Int = 0 set(value) { field = value } // верно // set(value) { x = value } // StackOverflowError } fun main() { Bad2().x = 1; println("ok") }Вычисляемое свойство выполняется при каждом чтении:
class Slow(val items: List<Int>) { val max: Int get() = items.also { println("считаем") }.max() } fun main() { val s = Slow(listOf(1, 2)) println(s.max) println(s.max) // "считаем" выведется дважды }Если вычисление дорогое, нужен
by lazyили хранимое свойство.
valне означает неизменяемость объекта - только то, что ссылку нельзя переприсвоить:class Box2 { val items = mutableListOf(1) } fun main() { val b = Box2() b.items.add(2) // разрешено println(b.items) // [1, 2] }
valс геттером может возвращать разное:class Clock { val now: Long get() = System.nanoTime() } fun main() { val c = Clock() println(c.now != c.now) // true — два разных значения }Поэтому умное приведение для таких свойств не работает. Подробнее: Умные приведения
lateinitне работает с примитивами и сval. ДляIntиспользуйтеDelegates.notNull()или значение-маркер.
lateinitскрывает незаданную зависимость до рантайма. Чем раньше объект получает всё необходимое (лучше - в конструкторе), тем меньше шансов наUninitializedPropertyAccessException.
by lazyс исключением внутри повторит попытку:class Risky { var attempts = 0 val value: String by lazy { attempts++ if (attempts < 2) error("первая попытка падает") "успех на попытке $attempts" } } fun main() { val r = Risky() runCatching { r.value } println(r.value) // успех на попытке 2 }Результат не кешируется, пока лямбда не завершится успешно.
Свойство-расширение не имеет поля - состояние к чужому классу добавить нельзя, только вычисление.
Публичный
varв API позволяет внешнему коду менять состояние в любой момент. Часто правильнееvarсprivate setи явным методом изменения.
Дополнительно: Делегирование Далее: Инициализация