Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
+433
View File
@@ -0,0 +1,433 @@
---
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)