Общие понятия
Из прошлого урока про модификаторы доступа плавно переходим к пониманию сеттеров и геттеров. Когда мы обращаемся к свойству какого-то класса, можно подумать, что мы делаем это напрямую. Однако, это не так. Kotlin под капотом генерирует так называемые сеттеры и геттеры. Это методы с помощью которых мы можем получать и изменять свойства. По аналогии с теми фукнциями, что мы сами писали на прошлом уроке, только на уровне языка. Напомню, мы создали getNumberOfPages() и setNumberOfPages(), чтобы получать и задавать значение количества страниц.
Воспроизведем для начала такую простую ситуацию – чтение и запись свойства класса. Создадим Вавилонскую рыбку, изменяемое поле будет хранить уровень нервного сигнала. Рыбка, напомню, занимается переводом языков используя сигналы мозга. В рабочем файле создаем экземпляр, считываем и обновляем значение. Все это распечатываем в консоль для наглядности.
class BabelFish {
var nerveSignalLevel: Int = 200
}
fun main() {
val fish = BabelFish()
println("old value: ${fish.nerveSignalLevel}")
fish.nerveSignalLevel = 400
println("new value: ${fish.nerveSignalLevel}")
}
Повторюсь, Kotlin в местах взаимодействия со свойством под капотом создает сеттеры и геттеры. Когда обращаемся к свойству – создается геттер, когда поставляем в него новое значение – сеттер. И такие сеттеры и геттеры при необходимости можно кастомизировать. То есть добавлять новые условия или данные при инициализации.
Геттер (getter)
Мы можем написать их явно. Для этого переходим в созданный класс и под свойством nerveSignalLevel сначала пропишем геттер. Это просто функция get(). После нее через равно пишем то значение, которое она будет возвращать при чтении этой переменной у экземпляра класса.
Реализация по умолчанию
Мы можем отдавать то значение, которым проинициализировали переменную по умолчанию. Делается это с помощью ключевого слова field. Оно передает то поле, для которого мы явно прописываем геттер. Если навести на слово курсор, увидим полное название нашей переменной. А если кликнуть, среда разработки подсветит относящуюся к нему переменную. Это реализация по умолчанию, которую прописывать явно не имеет смысла. Именно поэтому среда разработки подсвечивает геттер волнистой линией и предлагает удалить его как избыточный.
var nerveSignalLevel: Int = 200
get() = field
Произвольное значение
Другая ситуация, когда можно указать произвольное значение. Тогда оно и только оно будет возращаться при обращении к полю класса. Например, у нас переменная хранит 200, если тут проставим 250, запустив программу, увидим в консоли это значение.
var nerveSignalLevel: Int = 200
get() = 250
Сеттер (setter)
Реализация для нового значения
Окей. Разобрали геттер, теперь про сеттер. Эта функция вызывается при записи в переменную класса нового значения. Выглядит она так:
var nerveSignalLevel: Int = 200
get() = 250
set(value: Int) {
field = value
}
value в скобках – это объявленный параметр обычной функции, в него будет поступать значение. Название в принципе может быть любым. Тип автоматически будет таким же, как у значения переменной по умолчанию. Указывать его не обязательно, но для наглядности выведем его принудительно с помощью контекстного меню.
Ключевое слово field уже известно, его можно считать полем самого класса, для которого пишем сеттер. И ему присвамиваем поступающее в функцию значение из value. Мы написали реализацию сеттера по умолчанию. Для порядка проверим, передав какое-то значение в объект. Но сначала нужно откатить геттер до стандартной реализации, иначе он всегда будет возвращать 250.
class BabelFish {
var nerveSignalLevel: Int = 200
get() = field
set(value: Int) {
field = value
}
}
Кастомизация геттера/сеттера
Хорошо. Для кастомизации вместо value можно поставить какое-либо значение. Но предлагаю воспроизвести более осмысленную ситуацию. Пропишем дополнительную логику. Например, добавим Boolean переменную isTranslated со значением по умолчанию false. Ее смысл в том, что при достаточном уровне нервного сигнала Вавилонская рыбка справляется с переводом и переменная принимает значение true. И эту проверку можно добавить в сеттер.
class BabelFish {
var isTranslated: Boolean = false
var nerveSignalLevel: Int = 200
get() = field
set(value: Int) {
field = value
if (value > 300) {
isTranslated = true
println("isTranslated = true")
}
}
}
Теперь, если задать значение нервного сигнала больше 300, переменной isTranslated автоматически присвоится значение true. Посмотри как это выглядит.
fun main() {
val fish = BabelFish()
println("old value: ${fish.nerveSignalLevel}")
println("old value: ${fish.isTranslated}")
fish.nerveSignalLevel = 400
println("new value: ${fish.nerveSignalLevel}")
println("old value: ${fish.isTranslated}")
}
Итак, какие видим логи в консоли. Изначальное значение сигнала 200, isTranslated false. Присваиваем новое значение сигнала 400. Сеттер отрабатывает с методом распечатки isTranslated = true и проверяем поля заново. Сигнал 400 и isTranslated true.
Ну и для порядка кастомизируем геттер. Изменение его логики может пригодиться, когда при получении нужно, например, дополнительно вычислять значение. Воспроизведем такое поведение.
Добавим целочисленное свойство с коэффициентом изменения сигнала. Так как он может отсутствовать вовсе, сделаем свойство нуллабельным.
В логику геттера добавим сначала проверку на null. Если коэффициент не null, возвращаем поле с уровнем заданного сигнала помноженное на коэффициент. Иначе просто возвращаем field.
class BabelFish(
private val coefficient: Int?
) {
var isTranslated: Boolean = false
var nerveSignalLevel: Int = 200
get() = if (coefficient != null) field * coefficient else field
set(value: Int) {
field = value
if (value > 300) {
isTranslated = true
rintln("isTranslated = true")
}
}
}
Убедимся, что все работает. Создадим еще два объекта с разными коэффициентами. Теперь в консоль должен вывестись сначала уровень сигнала по умолчанию, затем с коэффициентом 2 и 21. Геттер отрабатывает корректно.
fun main() {
val fish = BabelFish(null)
println("old value: ${fish.nerveSignalLevel}")
// println("old value: ${fish.isTranslated}")
// fish.nerveSignalLevel = 400
// println("new value: ${fish.nerveSignalLevel}")
// println("old value: ${fish.isTranslated}")
val fish2 = BabelFish(2)
println("old value: ${fish2.nerveSignalLevel}")
val fish3 = BabelFish(21)
println("old value: ${fish3.nerveSignalLevel}")
}
Остается только сказать, что обязательно нужно использовать field и не пытаться использовать вместо него саму переменную класса. В противном случае произойдет рекурсивный (зацикленный) вызов этой переменной.
Для тех, кто собрался стать Android-разработчиком
Пошаговая
схема
Описание процесса обучения от основ Kotlin до Android-разработчика
Бесплатные
уроки
Авторский бесплатный курс по основам языка программирования Kotlin
Обучающий
бот
Тренажер и самоучитель по Котлин – бесплатные тесты и практика