Свойства
В начальном разделе вы узнали, как свойства используются для объявления характеристик экземпляров классов и как получать к ним доступ. В этой главе мы подробнее рассмотрим работу свойств в Kotlin и изучим другие способы их использования в коде.
Поля хранения
В Kotlin у свойств есть функции get() и set() по умолчанию, называемые методами доступа к свойствам. Они отвечают за получение и изменение значений свойств. Хотя эти функции по умолчанию явно не видны в коде, компилятор автоматически генерирует их для управления доступом к свойствам. Эти методы доступа используют поле хранения для хранения фактического значения свойства.
Поле хранения существует, если выполняется хотя бы одно из следующих условий:
Для свойства используются функции
get()илиset()по умолчанию.В коде предпринимается попытка получить доступ к значению свойства с помощью ключевого слова
field.
Например, в этом коде свойство 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]
}
Упражнение 2
Вам нужно, чтобы приложение для путешествий отображало расстояния и в километрах, и в милях. Создайте свойство-расширение для типа Double с именем asMiles, которое переводит расстояние в километрах в мили:
- Подсказка
Помните, что для свойств-расширений нужна пользовательская функция
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
}
Упражнение 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
}
Упражнение 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.
}
© 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