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

16 KiB
Raw Blame History

title, description, tags
title description tags
Свойства Геттеры и сеттеры, backing field, вычисляемые свойства, lateinit, lazy и делегаты
kotlin
oop

Свойство - это поле плюс методы доступа. Снаружи выглядит как поле, но обращение всегда идёт через геттер и сеттер.

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 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?

Свойства - не синтаксический сахар над геттерами, а единица абстракции. Переход от хранимого свойства к вычисляемому не меняет код клиентов:

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 и явным методом изменения.

Дополнительно: Делегирование Далее: Инициализация