--- title: Свойства description: Геттеры и сеттеры, backing field, вычисляемые свойства, lateinit, lazy и делегаты tags: - kotlin - oop --- Свойство - это поле плюс методы доступа. Снаружи выглядит как поле, но обращение всегда идёт через геттер и сеттер. ```kotlin class User { val id: Int = 1 // только чтение: геттер var name: String = "Иван" // чтение и запись: геттер + сеттер } fun main() { val u = User() println(u.id) // вызов геттера u.name = "Пётр" // вызов сеттера println(u.name) // Пётр } ``` Полная форма объявления: ```kotlin 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` - само хранилище. Прямое обращение к свойству по имени привело бы к бесконечной рекурсии. ```kotlin 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` появляется только если он реально нужен. У вычисляемого свойства хранилища нет: ```kotlin class Rect2(val width: Int, val height: Int) { val area: Int get() = width * height // поля нет, считается каждый раз } fun main() { println(Rect2(2, 3).area) // 6 } ``` Это важно для памяти: свойство-геттер не занимает места в объекте. ## Валидация в сеттере ```kotlin 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 } } ``` ## Видимость сеттера Частый приём - читать можно всем, менять только внутри: ```kotlin 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" // ошибка: сеттер приватный } ``` Это аккуратнее, чем приватное поле плюс отдельный публичный геттер. ## Вычисляемые свойства или функции ```kotlin class Order3(val items: List) { 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), обращение к сети. ```kotlin class Data2(val rows: List) { val size: Int get() = rows.size // дешёво — свойство fun parse(): List = rows.map { it.length } // работа — функция } fun main() { val d = Data2(listOf("ab", "cde")) println("${d.size} ${d.parse()}") // 2 [2, 3] } ``` ## Инициализация и порядок Свойство должно быть инициализировано при создании объекта: ```kotlin 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 } ``` Подробнее: [Инициализация](Инициализация.md) ## lateinit Когда значение появится позже, но хочется non-null тип: ```kotlin 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` доступна только внутри класса Обращение до инициализации даёт понятную ошибку: ```kotlin 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 Вычисляется один раз при первом обращении: ```kotlin class Config5 { val heavy: String by lazy { println("вычисляем...") "результат" } } fun main() { val c = Config5() println("создан") // создан println(c.heavy) // вычисляем... затем результат println(c.heavy) // результат — повторно не считается } ``` `lazy` работает только с `val`. По умолчанию потокобезопасен (синхронизация при первом доступе); если объект гарантированно в одном потоке, можно ослабить: ```kotlin val value: Int by lazy(LazyThreadSafetyMode.NONE) { 42 } fun main() { println(value) } // 42 ``` | | `lateinit var` | `by lazy` | |---|---|---| | изменяемость | `var` | `val` | | кто задаёт значение | внешний код | сама лямбда | | примитивы | нельзя | можно | | потокобезопасность | нет | есть по умолчанию | | доступ до инициализации | исключение | вычислит на месте | ## observable и vetoable Готовые делегаты для реакции на изменение: ```kotlin 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` для примитивов: ```kotlin 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` - удобно для разбора динамических данных: ```kotlin class User2(map: Map) { 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 } ``` ## Свой делегат ```kotlin 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}'") // 'иван' } ``` Подробнее: [Делегирование](Делегирование.md) ## Свойства верхнего уровня и расширения ```kotlin 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` - только геттер. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | поле + `getX()` + `setX()` | `var x` | > | `obj.getName()` | `obj.name` | > | `obj.setName("x")` | `obj.name = "x"` | > | `private final` поле | `val` | > | геттер с вычислением | `val x get() = ...` | > | Lombok `@Getter`/`@Setter` | не нужны | > | `Supplier` + кеш для ленивости | `by lazy` | > | `Optional` поле | `T?` | > > **Свойства - не синтаксический сахар над геттерами, а единица абстракции.** Переход от хранимого свойства к вычисляемому не меняет код клиентов: > ```kotlin > 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`: > ```kotlin > fun main() { > val sb = StringBuilder("abc") > println(sb.length) // вместо sb.length() — синтетическое свойство > } > ``` > > **`by lazy` заменяет двойную проверку блокировки.** Классический ленивый синглтон в Java требует `volatile` и синхронизации; в Kotlin это одна строка. > > Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md) > [!warning] Подводные камни > **Обращение к свойству внутри его сеттера - бесконечная рекурсия:** > ```kotlin > class Bad2 { > var x: Int = 0 > set(value) { field = value } // верно > // set(value) { x = value } // StackOverflowError > } > > fun main() { Bad2().x = 1; println("ok") } > ``` > > **Вычисляемое свойство выполняется при каждом чтении:** > ```kotlin > class Slow(val items: List) { > 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` не означает неизменяемость объекта** - только то, что ссылку нельзя переприсвоить: > ```kotlin > class Box2 { val items = mutableListOf(1) } > > fun main() { > val b = Box2() > b.items.add(2) // разрешено > println(b.items) // [1, 2] > } > ``` > > **`val` с геттером может возвращать разное:** > ```kotlin > class Clock { val now: Long get() = System.nanoTime() } > > fun main() { > val c = Clock() > println(c.now != c.now) // true — два разных значения > } > ``` > Поэтому умное приведение для таких свойств не работает. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md) > > **`lateinit` не работает с примитивами и с `val`.** Для `Int` используйте `Delegates.notNull()` или значение-маркер. > > **`lateinit` скрывает незаданную зависимость до рантайма.** Чем раньше объект получает всё необходимое (лучше - в конструкторе), тем меньше шансов на `UninitializedPropertyAccessException`. > > **`by lazy` с исключением внутри повторит попытку:** > ```kotlin > 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` и явным методом изменения. Дополнительно: [Делегирование](Делегирование.md) Далее: [Инициализация](Инициализация.md)