Свойства
В Kotlin свойства позволяют хранить данные и управлять ими без написания функций для доступа к этим данным или их изменения. Вы можете использовать свойства в классах, интерфейсах, объектах, объектах-компаньонах и даже вне этих структур — как свойства верхнего уровня.
У каждого свойства есть имя, тип и автоматически сгенерированная функция get(), которая называется геттером.
С помощью геттера можно прочитать значение свойства. Если свойство изменяемое, у него также есть функция set(),
которая называется сеттером и позволяет изменить значение свойства.
Геттеры и сеттеры называются методами доступа.
Объявление свойств
Свойства могут быть изменяемыми (var) или доступными только для чтения (val).
Их можно объявлять как свойства верхнего уровня в файле .kt. Свойство верхнего уровня можно считать глобальной переменной,
которая принадлежит пакету:
// Файл: Constants.kt
package my.app
val pi = 3.14159
var counter = 0
Вы также можете объявлять свойства внутри класса, интерфейса или объекта:
// Класс со свойствами
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
}
// Интерфейс со свойством
interface ContactInfo {
val email: String
}
// Объект со свойствами
object Company {
var name: String = "Detective Inc."
val country: String = "UK"
}
// Класс, реализующий интерфейс
class PersonContact : ContactInfo {
override val email: String = "[email protected]"
}
Чтобы использовать свойство, обратитесь к нему по имени:
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
}
interface ContactInfo {
val email: String
}
object Company {
var name: String = "Detective Inc."
val country: String = "UK"
}
class PersonContact : ContactInfo {
override val email: String = "[email protected]"
}
fun copyAddress(address: Address): Address {
val result = Address()
// Обращается к свойствам экземпляра result
result.name = address.name
result.street = address.street
result.city = address.city
return result
}
fun main() {
val sherlockAddress = Address()
val copy = copyAddress(sherlockAddress)
// Обращается к свойствам экземпляра copy
println("Copied address: ${copy.name}, ${copy.street}, ${copy.city}")
// Copied address: Holmes, Sherlock, Baker, London
// Обращается к свойствам объекта Company
println("Company: ${Company.name} in ${Company.country}")
// Company: Detective Inc. in UK
val contact = PersonContact()
// Обращается к свойствам экземпляра contact
println("Email: ${contact.email}")
// Email: [email protected]
}
В Kotlin рекомендуется инициализировать свойства при объявлении, чтобы код оставался безопасным и простым для чтения. Однако в особых случаях свойства можно инициализировать позже.
Указывать тип свойства необязательно, если компилятор может вывести его из инициализатора или из возвращаемого типа геттера:
var initialized = 1 // выведенный тип — Int
var allByDefault // ошибка: свойство должно быть инициализировано
Пользовательские геттеры и сеттеры
По умолчанию Kotlin автоматически генерирует геттеры и сеттеры. Вы можете определить собственные методы доступа, когда нужна дополнительная логика: например, валидация, форматирование или вычисления на основе других свойств.
Пользовательский геттер выполняется при каждом обращении к свойству:
class Rectangle(val width: Int, val height: Int) {
val area: Int
get() = this.width * this.height
}
fun main() {
val rectangle = Rectangle(3, 4)
println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}")
}
Тип можно опустить, если компилятор может вывести его из геттера:
val area get() = this.width * this.height
Пользовательский сеттер выполняется при каждом присваивании значения свойству, кроме инициализации.
По соглашению параметр сеттера называется value, но вы можете выбрать другое имя:
class Point(var x: Int, var y: Int) {
var coordinates: String
get() = "$x,$y"
set(value) {
val parts = value.split(",")
x = parts[0].toInt()
y = parts[1].toInt()
}
}
fun main() {
val location = Point(1, 2)
println(location.coordinates)
// 1,2
location.coordinates = "10,20"
println("${location.x}, ${location.y}")
// 10, 20
}
Изменение видимости или добавление аннотаций
В Kotlin можно изменить видимость метода доступа или добавить аннотации, не заменяя реализацию по умолчанию.
Для таких изменений не нужно объявлять тело {}.
Чтобы изменить видимость метода доступа, укажите модификатор перед ключевым словом get или set:
class BankAccount(initialBalance: Int) {
var balance: Int = initialBalance
// Только класс может изменять баланс
private set
fun deposit(amount: Int) {
if (amount > 0) balance += amount
}
fun withdraw(amount: Int) {
if (amount > 0 && amount <= balance) balance -= amount
}
}
fun main() {
val account = BankAccount(100)
println("Initial balance: ${account.balance}")
// 100
account.deposit(50)
println("After deposit: ${account.balance}")
// 150
account.withdraw(70)
println("After withdrawal: ${account.balance}")
// 80
// account.balance = 1000
// Ошибка: присваивание невозможно, потому что сеттер приватный
}
Чтобы аннотировать метод доступа, укажите аннотацию перед ключевым словом get или set:
// Определяет аннотацию, которую можно применить к геттеру
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class Inject
class Service {
var dependency: String = "Default Service"
// Аннотирует геттер
@Inject get
}
fun main() {
val service = Service()
println(service.dependency)
// Default service
println(service::dependency.getter.annotations)
// [@Inject()]
println(service::dependency.setter.annotations)
// []
}
В этом примере используется рефлексия, чтобы показать, какие аннотации присутствуют у геттера и сеттера.
Теневые поля
В Kotlin методы доступа используют теневые поля для хранения значения свойства в памяти. Теневые поля полезны, когда нужно добавить дополнительную логику в геттер или сеттер либо выполнить дополнительное действие при каждом изменении свойства.
Нельзя объявлять теневые поля напрямую. Kotlin генерирует их только при необходимости.
В методах доступа можно обратиться к теневому полю с помощью ключевого слова field.
Kotlin генерирует теневые поля только в том случае, если используется геттер или сеттер по умолчанию
либо если field используется хотя бы в одном пользовательском методе доступа.
Например, у свойства isEmpty нет теневого поля, потому что оно использует пользовательский геттер без ключевого слова field:
val isEmpty: Boolean
get() = this.size == 0
В этом примере у свойства score есть теневое поле, потому что сеттер использует ключевое слово field:
class Scoreboard {
var score: Int = 0
set(value) {
field = value
// Добавляет логирование при обновлении значения
println("Score updated to $field")
}
}
fun main() {
val board = Scoreboard()
board.score = 10
// Score updated to 10
board.score = 20
// Score updated to 20
}
Теневые свойства
Иногда может потребоваться больше гибкости, чем может дать теневое поле. Например, если в API нужно разрешить изменять свойство внутри класса, но запретить изменять его извне. В таких случаях можно использовать шаблон кода, который называется теневым свойством.
В следующем примере у класса ShoppingCart есть свойство items, которое представляет содержимое корзины покупок.
Нужно, чтобы свойство items было доступно только для чтения вне класса, но при этом оставался один “разрешённый” способ
напрямую изменять это свойство. Для этого можно определить приватное теневое свойство _items и публичное свойство items,
которое делегирует значение теневому свойству.
class ShoppingCart {
// Теневое свойство
private val _items = mutableListOf<String>()
// Публичное представление только для чтения
val items: List<String>
get() = _items
fun addItem(item: String) {
_items.add(item)
}
fun removeItem(item: String) {
_items.remove(item)
}
}
fun main() {
val cart = ShoppingCart()
cart.addItem("Apple")
cart.addItem("Banana")
println(cart.items)
// [Apple, Banana]
cart.removeItem("Apple")
println(cart.items)
// [Banana]
}
В этом примере пользователь может добавлять элементы в корзину только через функцию addItem(), но всё ещё может обращаться
к свойству items, чтобы посмотреть содержимое корзины.
Используйте подчёркивание в начале имени теневого свойства, чтобы следовать соглашениям по оформлению кода Kotlin.
На JVM компилятор оптимизирует доступ к приватным свойствам со стандартными методами доступа, чтобы избежать накладных расходов на вызов функции.
Теневые свойства также полезны, когда нужно, чтобы несколько публичных свойств разделяли одно состояние. Например:
class Temperature {
// Теневое свойство, которое хранит температуру в градусах Цельсия
private var _celsius: Double = 0.0
var celsius: Double
get() = _celsius
set(value) { _celsius = value }
var fahrenheit: Double
get() = _celsius * 9 / 5 + 32
set(value) { _celsius = (value - 32) * 5 / 9 }
}
fun main() {
val temp = Temperature()
temp.celsius = 25.0
println("${temp.celsius}°C = ${temp.fahrenheit}°F")
// 25.0°C = 77.0°F
temp.fahrenheit = 212.0
println("${temp.celsius}°C = ${temp.fahrenheit}°F")
// 100.0°C = 212.0°F
}
В этом примере к теневому свойству _celsius обращаются оба свойства: celsius и fahrenheit.
Такая схема предоставляет единый источник истины с двумя публичными представлениями.
Константы времени компиляции
Если значение свойства, доступного только для чтения, известно во время компиляции, пометьте его как константу времени компиляции
с помощью модификатора const. Константы времени компиляции подставляются в код во время компиляции, поэтому каждая ссылка
заменяется фактическим значением. Доступ к ним эффективнее, потому что геттер не вызывается:
// Файл: AppConfig.kt
package com.example
// Константа времени компиляции
const val MAX_LOGIN_ATTEMPTS = 3
Константы времени компиляции должны соответствовать следующим требованиям:
- Они должны быть свойством верхнего уровня либо членом объявления
objectили объекта-компаньона. - Они должны быть инициализированы значением типа
Stringили примитивного типа. - У них не может быть пользовательского геттера.
У констант времени компиляции всё равно есть теневое поле, поэтому с ними можно работать с помощью рефлексии.
Такие свойства также можно использовать в аннотациях:
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun processLegacyOrders() { ... }
Свойства и переменные с поздней инициализацией
Обычно свойства нужно инициализировать в конструкторе. Однако это не всегда удобно. Например, свойства можно инициализировать через внедрение зависимостей или внутри установочного метода модульного теста.
Чтобы обработать такие ситуации, пометьте свойство модификатором lateinit:
public class OrderServiceTest {
lateinit var orderService: OrderService
@SetUp fun setup() {
orderService = OrderService()
}
@Test fun processesOrderSuccessfully() {
// Вызывает orderService напрямую, без проверки на null
// или порядок инициализации
orderService.processOrder()
}
}
Модификатор lateinit можно использовать со свойствами var, объявленными как:
- Свойства верхнего уровня.
- Локальные переменные.
- Свойства внутри тела класса.
Для свойств класса:
- Их нельзя объявлять в основном конструкторе.
- У них не должно быть пользовательского геттера или сеттера.
Во всех случаях свойство или переменная должны иметь тип, не допускающий null, и не должны быть примитивного типа.
Если обратиться к свойству lateinit до его инициализации, Kotlin выбросит специальное исключение, которое указывает
на неинициализированное свойство:
class ReportGenerator {
lateinit var report: String
fun printReport() {
// Выбрасывает исключение, потому что свойство используется
// до инициализации
println(report)
}
}
fun main() {
val generator = ReportGenerator()
generator.printReport()
// Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property report has not been initialized
}
Чтобы проверить, было ли свойство lateinit var уже инициализировано, используйте свойство
isInitialized
на ссылке на это свойство:
class WeatherStation {
lateinit var latestReading: String
fun printReading() {
// Проверяет, инициализировано ли свойство
if (this::latestReading.isInitialized) {
println("Latest reading: $latestReading")
} else {
println("No reading available")
}
}
}
fun main() {
val station = WeatherStation()
station.printReading()
// No reading available
station.latestReading = "22°C, sunny"
station.printReading()
// Latest reading: 22°C, sunny
}
Использовать isInitialized для свойства можно только в том случае, если это свойство уже доступно из вашего кода.
Свойство должно быть объявлено в том же классе, во внешнем классе или как свойство верхнего уровня в том же файле.
Переопределение свойств
Делегированные свойства
Чтобы переиспользовать логику и уменьшить дублирование кода, можно делегировать ответственность за получение и установку значения свойства отдельному объекту.
Делегирование поведения методов доступа централизует логику доступа к свойству, упрощая её переиспользование. Такой подход полезен при реализации поведения вроде:
- Ленивого вычисления значения.
- Чтения из ассоциативного массива по заданному ключу.
- Доступа к базе данных.
- Уведомления слушателя при обращении к свойству.
Эти распространённые варианты поведения можно самостоятельно реализовать в библиотеках или использовать существующие делегаты из внешних библиотек. Подробнее см. в разделе делегированные свойства.