Files
obsidian/Kotlin/ООП/Свойства.md
T

434 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<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), обращение к сети.
```kotlin
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]
}
```
## Инициализация и порядок
Свойство должно быть инициализировано при создании объекта:
```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<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
}
```
## Свой делегат
```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>` поле | `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<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` не означает неизменяемость объекта** - только то, что ссылку нельзя переприсвоить:
> ```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)