Свойства

Объявление свойств

Свойства в классах Kotlin могут быть объявлены либо как изменяемые (mutable) и неизменяемые (read-only) — var и val соответственно.

class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
    var state: String? = null
    var zip: String = "123456"
}

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

fun copyAddress(address: Address): Address {
    val result = Address() // в Kotlin нет никакого слова `new`
    result.name = address.name // вызов методов доступа
    result.street = address.street
    // ...
    return result
}

Геттеры и сеттеры

Полный синтаксис объявления свойства выглядит так:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

Инициализатор property_initializer, геттер и сеттер можно не указывать. Также необязательно указывать тип свойства, если он может быть выведен из инициализатора или из возвращаемого типа геттера.

var initialized = 1 // имеет тип Int, стандартный геттер и сеттер
// var allByDefault // ошибка: необходима явная инициализация, 
                    // предусмотрены стандартные геттер и сеттер

Синтаксис объявления констант имеет два отличия от синтаксиса объявления изменяемых переменных: во-первых, объявление константы начинается с ключевого слова val вместо var, а во-вторых, объявление сеттера запрещено.

val simple: Int? // имеет тип Int, стандартный геттер, 
                 // должен быть инициализирован в конструкторе
val inferredType = 1 // имеет тип Int и стандартный геттер

Вы можете самостоятельно определить методы доступа для свойства. Если вы определяете пользовательский геттер, он будет вызываться каждый раз, когда вы обращаетесь к свойству (таким образом, вы можете реализовать вычисляемое свойство). Вот пример пользовательского геттера:

class Rectangle(val width: Int, val height: Int) {
    val area: Int
        get() = this.width * this.height // тип свойства необязателен, поскольку он может быть выведен из возвращаемого типа геттера
}

Вы можете опустить тип свойства, если его можно определить с помощью геттера.

val area get() = this.width * this.height

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

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // парсит строку и устанавливает 
                                 // значения для других свойств
    }

По договорённости, имя параметра сеттера - value, но вы можете использовать любое другое.

Если вам нужно изменить область видимости метода доступа или пометить его аннотацией, при этом не внося изменения в реализацию по умолчанию, вы можете объявить метод доступа без объявления его тела.

var setterVisibility: String = "abc"
    private set // сеттер имеет private доступ и стандартную реализацию

var setterWithAnnotation: Any? = null
    @Inject set // аннотирование сеттера с помощью Inject

Теневые поля

В Kotlin поле используется только как часть свойства для хранения его значения в памяти. Поля не могут быть объявлены напрямую. Однако, когда свойству требуется теневое поле (backing field), Kotlin предоставляет его автоматически. На это теневое поле можно обратиться в методах доступа, используя идентификатор field:

var counter = 0 // инициализатор назначает резервное поле напрямую
    set(value) {
        if (value >= 0)
            field = value // значение при инициализации записывается 
                          // прямиком в backing field

            // counter = value // ERROR StackOverflow: Использование 'counter' сделало бы сеттер рекурсивным
    }

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

Теневое поле будет сгенерировано для свойства, если оно использует стандартную реализацию как минимум одного из методов доступа, либо если пользовательский метод доступа ссылается на него через идентификатор field.

Например, в примере ниже не будет никакого теневого поля:

val isEmpty: Boolean
    get() = this.size == 0

Теневые свойства

Если вы хотите предпринять что-то такое, что выходит за рамки вышеуказанной схемы неявного теневого поля, вы всегда можете использовать теневое свойство (backing property).

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // параметры типа вычисляются автоматически 
                               // (ориг.: "Type parameters are inferred")
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

В JVM: доступ к приватным свойствам со стандартными геттерами и сеттерами оптимизируется таким образом, что вызов функции не происходит.

Константы времени компиляции

Если значение константного (read-only) свойства известно во время компиляции, пометьте его как константы времени компиляции, используя модификатор const. Такие свойства должны соответствовать следующим требованиям:

  • Находиться на самом высоком уровне или быть членами объявления object или вспомогательного объекта;
  • Быть проинициализированными значением типа String или значением примитивного типа;
  • Не иметь переопределённого геттера.

Такие свойства могут быть использованы в аннотациях.

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }

Свойства и переменные с поздней инициализацией

Обычно, свойства, объявленные non-null типом, должны быть проинициализированы в конструкторе. Однако часто бывает так, что делать это неудобно. К примеру, свойства могут быть инициализированы через внедрение зависимостей или в установочном методе (ориг.: setup method) юнит-теста. В таком случае вы не можете обеспечить non-null инициализацию в конструкторе, но всё равно хотите избежать проверок на null при обращении внутри тела класса к такому свойству.

Для того чтобы справиться с такой задачей, вы можете пометить свойство модификатором lateinit.

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method() // объект инициализирован, проверять на null не нужно
    }
}

Такой модификатор может быть использован только с var свойствами, объявленными внутри тела класса (не в основном конструкторе, и только тогда, когда свойство не имеет пользовательских геттеров и сеттеров), со свойствами верхнего уровня и локальными переменными. Тип такого свойства должен быть non-null и не должен быть примитивным.

Доступ к lateinit свойству до того, как оно проинициализировано, выбрасывает специальное исключение, которое чётко обозначает свойство, к которому осуществляется доступ, и тот факт, что оно не было инициализировано.

Проверка инициализации lateinit var

Чтобы проверить, было ли проинициализировано lateinit var свойство, используйте .isInitialized метод ссылки на это свойство.

if (foo::bar.isInitialized) {
    println(foo.bar)
}

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

Переопределение свойств

См. Переопределение свойств класса.

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

Самый простой тип свойств просто считывает (или записывает) данные из теневого поля. Тем не менее с пользовательскими геттерами и сеттерами мы можем реализовать совершенно любое поведение свойства. Где-то между простотой первого вида и разнообразием второго существуют общепринятые шаблоны того, что могут делать свойства. Несколько примеров: вычисление значения свойства при первом доступе к нему (ленивые значения), чтение из ассоциативного списка с помощью заданного ключа, доступ к базе данных, оповещение listener’а в момент доступа.

Такие распространённые поведения свойств могут быть реализованы в виде библиотек с помощью делегированных свойств.