Spec-Zone.ru › Kotlin 2

Свойства

В начальном разделе вы узнали, как свойства используются для объявления характеристик экземпляров классов и как получать к ним доступ. В этой главе мы подробнее рассмотрим работу свойств в Kotlin и изучим другие способы их использования в коде.

Поля хранения

В Kotlin у свойств есть функции get() и set() по умолчанию, называемые методами доступа к свойствам. Они отвечают за получение и изменение значений свойств. Хотя эти функции по умолчанию явно не видны в коде, компилятор автоматически генерирует их для управления доступом к свойствам. Эти методы доступа используют поле хранения для хранения фактического значения свойства.

Поле хранения существует, если выполняется хотя бы одно из следующих условий:

  • Для свойства используются функции get() или set() по умолчанию.

  • В коде предпринимается попытка получить доступ к значению свойства с помощью ключевого слова field.

Функции get() и set() также называются геттерами и сеттерами.

Например, в этом коде свойство category не имеет пользовательских функций get() или set(), поэтому используются реализации по умолчанию:

class Contact(val id: Int, var email: String) {
    var category: String = ""
}

Под капотом это эквивалентно следующему псевдокоду:

class Contact(val id: Int, var email: String) {
    var category: String = ""
        get() = field
        set(value) {
            field = value
        }
}

В этом примере:

  • Функция get() получает значение свойства из поля: "".

  • Функция set() принимает value в качестве параметра и присваивает его полю, где value — это "".

Доступ к полю хранения полезен, когда нужно добавить дополнительную логику в функции get() или set(), не вызывая бесконечный цикл. Например, у вас есть класс Person со свойством name:

class Person {
    var name: String = ""
}

Вы хотите, чтобы первая буква свойства name была заглавной, поэтому создаёте пользовательскую функцию set(), использующую функции-расширения .replaceFirstChar() и .uppercase(). Однако если в функции set() обращаться непосредственно к свойству, возникнет бесконечный цикл и во время выполнения произойдёт StackOverflowError:

class Person {
    var name: String = ""
        set(value) {
            // This causes a runtime error
            name = value.replaceFirstChar { firstChar -> firstChar.uppercase() }
        }
}

fun main() {
    val person = Person()
    person.name = "kodee"
    println(person.name)
    // Exception in thread "main" java.lang.StackOverflowError
}

Чтобы исправить это, вместо обращения к свойству можно использовать поле хранения в функции set(), обратившись к нему с помощью ключевого слова field:

class Person {
    var name: String = ""
        set(value) {
            field = value.replaceFirstChar { firstChar -> firstChar.uppercase() }
        }
}

fun main() {
    val person = Person()
    person.name = "kodee"
    println(person.name)
    // Kodee
}

Поля хранения также полезны, когда нужно добавить ведение журнала, отправлять уведомления при изменении значения свойства или использовать дополнительную логику для сравнения старого и нового значений свойства.

Дополнительные сведения см. в разделе Поля хранения.

Свойства-расширения

Как и функции-расширения, существуют также свойства-расширения. Свойства-расширения позволяют добавлять новые свойства к существующим классам, не изменяя их исходный код. Однако в Kotlin у свойств-расширений нет полей хранения. Это означает, что функции get() и set() нужно написать самостоятельно. Кроме того, отсутствие поля хранения означает, что такие свойства не могут хранить состояние.

Чтобы объявить свойство-расширение, укажите имя класса, который хотите расширить, затем . и имя свойства. Как и для обычных свойств класса, необходимо объявить тип свойства. Например:

val String.lastChar: Char

Свойства-расширения особенно полезны, когда нужно, чтобы свойство содержало вычисляемое значение, но при этом не использовать наследование. Можно считать, что свойства-расширения работают как функция с одним параметром — получателем.

Например, предположим, что у вас есть класс данных Person с двумя свойствами: firstName и lastName.

data class Person(val firstName: String, val lastName: String)

Вы хотите получать полное имя человека, не изменяя класс данных Person и не наследуясь от него. Для этого можно создать свойство-расширение с пользовательской функцией get():

data class Person(val firstName: String, val lastName: String)

// Extension property to get the full name
val Person.fullName: String
    get() = "$firstName $lastName"

fun main() {
    val person = Person(firstName = "John", lastName = "Doe")

    // Use the extension property
    println(person.fullName)
    // John Doe
}

Свойства-расширения не могут переопределять существующие свойства класса.

Как и функции-расширения, стандартная библиотека Kotlin широко использует свойства-расширения. Например, см. свойство lastIndex для CharSequence.

Делегированные свойства

В главе Классы и интерфейсы вы уже познакомились с делегированием. Делегирование можно использовать и со свойствами, чтобы передать методы доступа к ним другому объекту. Это полезно, когда требования к хранению свойств сложнее, чем может обеспечить простое поле хранения, например если значения нужно хранить в таблице базы данных, сеансе браузера или карте. Использование делегированных свойств также сокращает шаблонный код, поскольку логика получения и задания значений свойств содержится только в объекте, которому делегируется управление.

Синтаксис похож на делегирование классов, но работает на другом уровне. Объявите свойство, затем укажите ключевое слово by и объект, которому хотите делегировать управление. Например:

val displayName: String by Delegate

Здесь делегированное свойство displayName использует объект Delegate для своих методов доступа.

У каждого объекта, которому делегируется управление, должна быть операторная функция getValue(), с помощью которой Kotlin получает значение делегированного свойства. Если свойство изменяемое, у объекта также должна быть операторная функция setValue(), позволяющая Kotlin задать его значение.

По умолчанию функции getValue() и setValue() имеют следующий вид:

operator fun getValue(thisRef: Any?, property: KProperty<*>): String {}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {}

В этих функциях:

  • Ключевое слово operator помечает эти функции как операторные, позволяя им переопределять функции get() и set().

  • Параметр thisRef указывает на объект, содержащий делегированное свойство. По умолчанию его тип — Any?, но может потребоваться объявить более конкретный тип.

  • Параметр property указывает на свойство, значение которого получается или изменяется. С его помощью можно получить такие сведения, как имя или тип свойства. По умолчанию его тип — KProperty<*>, но также можно использовать Any?. Изменять это в коде не требуется.

По умолчанию функция getValue() возвращает значение типа String, но при желании это можно изменить.

У функции setValue() есть дополнительный параметр value, предназначенный для нового значения, присваиваемого свойству.

Как это выглядит на практике? Предположим, вам нужно вычисляемое свойство, например отображаемое имя пользователя, которое вычисляется только один раз, поскольку операция затратна, а приложение чувствительно к производительности. Можно использовать делегированное свойство для кэширования отображаемого имени: оно будет вычислено только один раз, но к нему можно будет обращаться в любой момент без снижения производительности.

Сначала нужно создать объект, которому будет делегировано управление. В этом случае объект будет экземпляром класса CachedStringDelegate:

class CachedStringDelegate {
    var cachedValue: String? = null
}

Свойство cachedValue содержит кэшированное значение. В классе CachedStringDelegate добавьте в тело операторной функции getValue() поведение, которое должно выполняться в функции get() делегированного свойства:

class CachedStringDelegate {
    var cachedValue: String? = null

    operator fun getValue(thisRef: Any?, property: Any?): String {
        if (cachedValue == null) {
            cachedValue = "Default Value"
            println("Computed and cached: $cachedValue")
        } else {
            println("Accessed from cache: $cachedValue")
        }
        return cachedValue ?: "Unknown"
    }
}

Функция getValue() проверяет, имеет ли свойство cachedValue значение null. Если да, функция присваивает "Default value" и выводит строку для ведения журнала. Если свойство cachedValue уже было вычислено, оно не является null. В этом случае для ведения журнала выводится другая строка. Наконец, функция использует оператор Элвиса, чтобы вернуть кэшированное значение или "Unknown", если значение равно null.

Теперь можно делегировать свойство, которое нужно кэшировать (val displayName), экземпляру класса CachedStringDelegate:

class CachedStringDelegate {
    var cachedValue: String? = null

    operator fun getValue(thisRef: User, property: Any?): String {
        if (cachedValue == null) {
            cachedValue = "${thisRef.firstName} ${thisRef.lastName}"
            println("Computed and cached: $cachedValue")
        } else {
            println("Accessed from cache: $cachedValue")
        }
        return cachedValue ?: "Unknown"
    }
}

class User(val firstName: String, val lastName: String) {
    val displayName: String by CachedStringDelegate()
}

fun main() {
    val user = User("John", "Doe")

    // First access computes and caches the value
    println(user.displayName)
    // Computed and cached: John Doe
    // John Doe

    // Subsequent accesses retrieve the value from cache
    println(user.displayName)
    // Accessed from cache: John Doe
    // John Doe
}

В этом примере:

  • Создаётся класс User с двумя свойствами в заголовке — firstName и lastName — и одним свойством в теле класса — displayName.

  • Свойство displayName делегируется экземпляру класса CachedStringDelegate.

  • Создаётся экземпляр класса User с именем user.

  • Выводится результат обращения к свойству displayName экземпляра user.

Обратите внимание: в функции getValue() тип параметра thisRef сужается с типа Any? до типа объекта: User. Это позволяет компилятору получать доступ к свойствам firstName и lastName класса User.

Стандартные делегаты

Стандартная библиотека Kotlin предоставляет несколько полезных делегатов, поэтому не всегда нужно создавать их с нуля. При использовании одного из этих делегатов определять функции getValue() и setValue() не нужно: стандартная библиотека предоставляет их автоматически.

Ленивые свойства

Чтобы инициализировать свойство только при первом обращении к нему, используйте ленивое свойство. Стандартная библиотека предоставляет интерфейс Lazy для делегирования.

Чтобы создать экземпляр интерфейса Lazy, используйте функцию lazy() и передайте ей лямбда-выражение, которое будет выполнено при первом вызове функции get(). Все последующие вызовы функции get() возвращают тот же результат, что и первый вызов. В ленивых свойствах для передачи лямбда-выражения используется синтаксис лямбды в конце.

Например:

class Database {
    fun connect() {
        println("Connecting to the database...")
    }

    fun query(sql: String): List<String> {
        return listOf("Data1", "Data2", "Data3")
    }
}

val databaseConnection: Database by lazy {
    val db = Database()
    db.connect()
    db
}

fun fetchData() {
    val data = databaseConnection.query("SELECT * FROM data")
    println("Data: $data")
}

fun main() {
    // First time accessing databaseConnection
    fetchData()
    // Connecting to the database...
    // Data: [Data1, Data2, Data3]

    // Subsequent access uses the existing connection
    fetchData()
    // Data: [Data1, Data2, Data3]
}

В этом примере:

  • Есть класс Database с функциями-членами connect() и query().

  • Функция connect() выводит строку в консоль, а функция query() принимает SQL-запрос и возвращает список.

  • Есть ленивое свойство databaseConnection.

  • Лямбда-выражение, переданное функции lazy():

    • Создаёт экземпляр класса Database.

    • Вызывает функцию-член connect() для этого экземпляра (db).

    • Возвращает экземпляр.

  • Есть функция fetchData(), которая:

    • Создаёт SQL-запрос, вызывая функцию query() для свойства databaseConnection.

    • Присваивает SQL-запрос переменной data.

    • Выводит переменную data в консоль.

  • Функция main() вызывает функцию fetchData(). При первом вызове инициализируется ленивое свойство. При втором вызове возвращается тот же результат, что и при первом.

Ленивые свойства полезны не только в случаях, когда инициализация требует значительных ресурсов, но и когда свойство может вообще не использоваться в коде. Кроме того, ленивые свойства по умолчанию потокобезопасны, что особенно полезно при работе в конкурентной среде.

Дополнительные сведения см. в разделе Ленивые свойства.

Наблюдаемые свойства

Чтобы отслеживать изменения значения свойства, используйте наблюдаемое свойство. Оно полезно, когда нужно обнаруживать изменение значения свойства и реагировать на него. Стандартная библиотека предоставляет объект Delegates для делегирования.

Чтобы создать наблюдаемое свойство, сначала импортируйте kotlin.properties.Delegates.observable. Затем используйте функцию observable() и передайте ей лямбда-выражение, которое будет выполняться при каждом изменении свойства. Как и в случае с ленивыми свойствами, для передачи лямбда-выражения используется синтаксис лямбды в конце.

Например:

import kotlin.properties.Delegates.observable

class Thermostat {
    var temperature: Double by observable(20.0) { _, old, new ->
        if (new > 25) {
            println("Warning: Temperature is too high! ($old°C -> $new°C)")
        } else {
            println("Temperature updated: $old°C -> $new°C")
        }
    }
}

fun main() {
    val thermostat = Thermostat()
    thermostat.temperature = 22.5
    // Temperature updated: 20.0°C -> 22.5°C

    thermostat.temperature = 27.0
    // Warning: Temperature is too high! (22.5°C -> 27.0°C)
}

В этом примере:

  • Есть класс Thermostat, содержащий наблюдаемое свойство: temperature.

  • Функция observable() принимает 20.0 в качестве параметра и использует его для инициализации свойства.

  • Лямбда-выражение, переданное функции observable():

    • Имеет три параметра:

      • _, который указывает на само свойство.

      • old, который содержит старое значение свойства.

      • new, который содержит новое значение свойства.

    • Проверяет, больше ли параметр new, чем 25, и в зависимости от результата выводит строку в консоль.

  • Функция main():

    • Создаёт экземпляр класса Thermostat с именем thermostat.

    • Изменяет значение свойства temperature этого экземпляра на 22.5, что вызывает вывод сообщения об обновлении температуры.

    • Изменяет значение свойства temperature этого экземпляра на 27.0, что вызывает вывод предупреждения.

Наблюдаемые свойства полезны не только для ведения журнала и отладки. Их также можно использовать, например, для обновления пользовательского интерфейса или выполнения дополнительных проверок, таких как проверка достоверности данных.

Дополнительные сведения см. в разделе Наблюдаемые свойства.

Практика

Упражнение 1

Вы управляете системой учёта запасов в книжном магазине. Запасы хранятся в списке, где каждый элемент обозначает количество экземпляров определённой книги. Например, listOf(3, 0, 7, 12) означает, что в магазине есть 3 экземпляра первой книги, 0 — второй, 7 — третьей и 12 — четвёртой.

Напишите функцию с именем findOutOfStockBooks(), которая возвращает список индексов всех книг, которых нет в наличии.

Подсказка 1

Используйте свойство-расширение indices из стандартной библиотеки.

Подсказка 2

Вместо того чтобы вручную создавать и возвращать изменяемый список, можно использовать функцию buildList() для его создания и заполнения. Функция buildList() использует лямбду с получателем, с которой вы познакомились в предыдущих главах.

fun findOutOfStockBooks(inventory: List<Int>): List<Int> {
    // Write your code here
}

fun main() {
    val inventory = listOf(3, 0, 7, 0, 5)
    println(findOutOfStockBooks(inventory))
    // [1, 3]
}
fun findOutOfStockBooks(inventory: List<Int>): List<Int> { val outOfStockIndices = mutableListOf<Int>() for (index in inventory.indices) { if (inventory[index] == 0) { outOfStockIndices.add(index) } } return outOfStockIndices } fun main() { val inventory = listOf(3, 0, 7, 0, 5) println(findOutOfStockBooks(inventory)) // [1, 3] }
fun findOutOfStockBooks(inventory: List<Int>): List<Int> = buildList { for (index in inventory.indices) { if (inventory[index] == 0) { add(index) } } } fun main() { val inventory = listOf(3, 0, 7, 0, 5) println(findOutOfStockBooks(inventory)) // [1, 3] }

Упражнение 2

Вам нужно, чтобы приложение для путешествий отображало расстояния и в километрах, и в милях. Создайте свойство-расширение для типа Double с именем asMiles, которое переводит расстояние в километрах в мили:

Формула перевода километров в мили: miles = kilometers * 0.621371.

Подсказка

Помните, что для свойств-расширений нужна пользовательская функция get().

val // Write your code here

fun main() {
    val distanceKm = 5.0
    println("$distanceKm km is ${distanceKm.asMiles} miles")
    // 5.0 km is 3.106855 miles

    val marathonDistance = 42.195
    println("$marathonDistance km is ${marathonDistance.asMiles} miles")
    // 42.195 km is 26.218757 miles
}
val Double.asMiles: Double get() = this * 0.621371 fun main() { val distanceKm = 5.0 println("$distanceKm km is ${distanceKm.asMiles} miles") // 5.0 km is 3.106855 miles val marathonDistance = 42.195 println("$marathonDistance km is ${marathonDistance.asMiles} miles") // 42.195 km is 26.218757 miles }

Упражнение 3

У вас есть система проверки работоспособности, которая определяет состояние облачной системы. Однако две функции, выполняющие эту проверку, требуют значительных ресурсов. Используйте ленивые свойства для инициализации проверок, чтобы затратные функции запускались только при необходимости:

fun checkAppServer(): Boolean {
    println("Performing application server health check...")
    return true
}

fun checkDatabase(): Boolean {
    println("Performing database health check...")
    return false
}

fun main() {
    // Write your code here

    when {
        isAppServerHealthy -> println("Application server is online and healthy")
        isDatabaseHealthy -> println("Database is healthy")
        else -> println("System is offline")
    }
    // Performing application server health check...
    // Application server is online and healthy
}
fun checkAppServer(): Boolean { println("Performing application server health check...") return true } fun checkDatabase(): Boolean { println("Performing database health check...") return false } fun main() { val isAppServerHealthy by lazy { checkAppServer() } val isDatabaseHealthy by lazy { checkDatabase() } when { isAppServerHealthy -> println("Application server is online and healthy") isDatabaseHealthy -> println("Database is healthy") else -> println("System is offline") } // Performing application server health check... // Application server is online and healthy }

Упражнение 4

Вы создаёте простое приложение для ведения бюджета. Приложение должно отслеживать изменения оставшегося бюджета пользователя и уведомлять его, когда сумма опускается ниже заданного порога. У вас есть класс Budget, инициализируемый свойством totalBudget, содержащим начальную сумму бюджета. Внутри класса создайте наблюдаемое свойство с именем remainingBudget, которое выводит:

  • Предупреждение, если значение меньше 20% от начального бюджета.

  • Подбадривающее сообщение, если бюджет увеличился по сравнению с предыдущим значением.

import kotlin.properties.Delegates.observable

class Budget(val totalBudget: Int) {
    var remainingBudget: Int // Write your code here
}

fun main() {
    val myBudget = Budget(totalBudget = 1000)
    myBudget.remainingBudget = 800
    myBudget.remainingBudget = 150
    // Warning: Your remaining budget (150) is below 20% of your total budget.
    myBudget.remainingBudget = 50
    // Warning: Your remaining budget (50) is below 20% of your total budget.
    myBudget.remainingBudget = 300
    // Good news: Your remaining budget increased to 300.
}
import kotlin.properties.Delegates.observable class Budget(val totalBudget: Int) { var remainingBudget: Int by observable(totalBudget) { _, oldValue, newValue -> if (newValue < totalBudget * 0.2) { println("Warning: Your remaining budget ($newValue) is below 20% of your total budget.") } else if (newValue > oldValue) { println("Good news: Your remaining budget increased to $newValue.") } } } fun main() { val myBudget = Budget(totalBudget = 1000) myBudget.remainingBudget = 800 myBudget.remainingBudget = 150 // Warning: Your remaining budget (150) is below 20% of your total budget. myBudget.remainingBudget = 50 // Warning: Your remaining budget (50) is below 20% of your total budget. myBudget.remainingBudget = 300 // Good news: Your remaining budget increased to 300. }
  • Предыдущий шаг

  • Следующий шаг

01 июля 2026
Открытые классы и классы со спецификатором specialБезопасность работы с null

© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/kotlin-tour-intermediate-properties.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API