434 lines
16 KiB
Markdown
434 lines
16 KiB
Markdown
---
|
||
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)
|