Геттеры и сеттеры в Kotlin, блок init
В Kotlin обращение к свойству объекта за его значением, например a.number , не то, чем кажется на самом деле. Все немного сложнее. Мы не просто запрашиваем значение переменной number , а делаем это опосредовано, через метод get() – так называемый геттер (getter). Явное использование геттера выглядит так:
class NumInc(n: Int = 0, gap: Int = 1) { var number = n get() { return field } var step = gap get() { return field } fun inc() { number += step } fun dec() { number -= step } }
Геттер – это особая функция, привязанная к свойству, после которого упоминается. То есть сколько в классе свойств, столько и геттеров. В теле данной функции описывается, что надо сделать со значением поля, прежде чем его вернуть. Если достаточно просто вернуть значение как оно есть, то явно get() можно не писать, таким он создается автоматически, чем мы и пользовались в предыдущих уроках.
В теле get() используется не само имя поля, оно заменяется на ключевое слово field . Это необходимо, чтобы избежать рекурсивных вызовов get() , то есть когда из тела функции вызывается эта же функция, из нее опять она же и так далее. Если мы будем использовать собственное имя поля в теле get() , получится что запрашиваем значение этого поля, а значит снова вызываем get() .
К явному описанию в классе геттера для свойства прибегают, когда значение надо вычислить или как-либо изменить перед его возвратом.
Введем в программу третье свойство – stepNoOne , значение которого вычисляется в момент обращения к нему. Свойство принимает значение true , если шаг не равен единице, иначе возвращается false . Причем новое значение вычисляется каждый раз, когда происходит обращение к полю.
class NumInc(var number: Int = 0, var step: Int = 1) { val stepNoOne: Boolean get() { return step != 1 } fun inc() { number += step } fun dec() { number -= step } }
Кроме геттера каждое поле имеет сеттер (setter) – метод set() . Он вызывается, когда свойству присваивается новое значение. Например, a.number = 34 .
В простейшем случае, который работает по умолчанию, то есть когда сеттер можно явно не писать, он выглядит так:
var number = 0 set(value) { field = value }
Здесь value – параметр функции set() . Вместо value можно использовать любое другое имя переменной. Данному параметру присваивается значение, которое мы хотим присвоить свойству. Например, когда выполняется выражение a.number = 34 , то value присваивается 34. В теле set() значение присваивается полю. При этом вместо его настоящего имени (в данном случае number ) используется ключевое слово field по тем же причинам, что и в случае геттера.
Зачем нужен сеттер? Бывает перед присвоением свойству надо обработать, видоизменить переданное значение. Например, мы не хотим, чтобы шаг мог устанавливаться в нечетное число, только в четное.
class NumInc(var number: Int = 0, step: Int = 2) { var step = step set(value) { if (value % 2 == 0) field = value else { field = value - 1 printWarning() } } private fun printWarning() { println("Шаг был установлен на 1 меньше") } fun inc() { number += step } fun dec() { number -= step } }
Обратите внимание на ключевое слово private в начале объявления функции printWarning() . Это один из модификаторов видимости. В данном случае private запрещает функции printWarning() быть видимой за пределами класса. В main мы не можем написать b.printWarning() . Приватная функция призвана обслуживать лишь внутренние дела класса и может вызываться только в пределах класса. Было бы странно, если бы объекты, созданные от класса NumInc , могли выводить надпись «Шаг был установлен на 1 меньше», даже когда подобного не происходит. Данное предупреждение должно выводиться лишь в момент установки шага, и если он не кратен двум.
Нередко требуется не просто проверить и изменить присваиваемое полю значение, а в принципе запретить изменение свойства. Если его значение устанавливается в момент создания объекта и потом не должно меняться, то выражение вроде a.number = 34 должно быть запрещено.
Если переменная-свойство неизменяемая, то есть была объявлена с модификатором val , мы не сможем менять ее значение везде. И за пределами класса, и в теле.
Можем оставить свойства с модификатором var , однако сделать их приватными, то есть недоступными из вне:
class NumInc(n: Int, gap: Int) { private var number = n private var step = gap fun inc() { number += step } fun dec() { number -= step } }
Однако в этом случае мы лишаемся не только возможности назначать свойствам новые значения за пределами класса, но и запрашивать их значения.
Поэтому, если требуется предоставить право получать значение свойства, но запретить его перезапись из вне, приватным делается только сеттер. По умолчанию и геттер и сеттер публичные (имеют модификатор public , который в Kotlin обычно не пишут).
var number = n private set
В этом случае присвоение свойству запрещено, считывание разрешено.
Вернемся к варианту, когда мы проверяли на кратность. Сработает ли сеттер при создании объекта? То есть можем ли мы установить нечетный шаг через конструктор? Если так, то это «дыра» в программе, при условии что шаг всегда должен быть четным.
class NumInc(num: Int, gap: Int) { var number = num var step = gap set(value) { if (step % 2 == 0) field = value else { field = value - 1 printWarning() } } private fun printWarning() { println("Шаг был установлен на 1 меньше") } }
Как видно на скрине сеттер не сработал.
Исправить ситуацию можно с помощью добавления в класс так называемого инициализатора – блока init<> .
class NumInc(num: Int, gap: Int) { var number = num var step = gap set(value) { if (step % 2 == 0) field = value else { field = value - 1 printWarning() } } init { if (gap % 2 != 0) { step = gap - 1 printWarning() } } private fun printWarning() { println("Шаг был установлен на 1 меньше") } }
Однако такая программа будет работать неправильно. Если main выглядит так:
fun main() { val b = NumInc(10, 5) println(b.number) println(b.step) }
то результат выполнения окажется таким:
Шаг был установлен на 1 меньше Шаг был установлен на 1 меньше 10 3
Почему единица была вычтена из шага два раза? Внутри init<> мы выполняем присваивание полю step и в этот момент set() срабатывает. Потом мы еще раз уменьшаем шаг уже в теле init<> . Поэтому правильный инициализатор в нашем случае должен выглядеть проще:
В классе может быть несколько блоков init<> :
class NumInc (num: Int, gap: Int) { var number = num init { if (num 0) number = -num } var step = gap set(value) { if (step % 2 == 0) field = value else { field = value - 1 printWarning() } } init { step = gap } private fun printWarning() { println("Шаг был установлен на 1 меньше") } }
Также можно использовать один общий для всех полей:
init { step = gap if (num 0) number = -num }
Блоки инициализации связаны с первичным конструктором. Они нужны лишь потому, что в то время как вторичный конструктор имеет тело, которое может содержать программный код для предварительной обработки значений перед их присваиванием свойствам, у первичного конструктора нет такого тела. Поэтому код обработки, если он нужен, помещается в блок init . Однако в таких случаях может быть проще отказаться от первичного конструктора и использовать только вторичные.
Напишите класс с тремя свойствами. Значения для первых двух устанавливаются с помощью конструктора, в дальнейшем их можно как получать, так изменять через объекты класса. Значение третьего свойства можно только получать, но не менять. Это значение должно представлять из себя строку, содержащую информацию о текущих значениях двух других свойств.
Введение в объектно-ориентированное программирование на Kotlin
Kotlin getter and setters
Геттеры (getter) и сеттеры (setter) (еще их называют методами доступа) позволяют управлять доступом к переменной. Их формальный синтаксис:
var имя_свойства[: тип_свойства] [= инициализатор_свойства] [getter] [setter]
Инициализатор, геттер и сеттер свойства необязательны. Указывать тип свойства также необязательно, если он может быть выведен их значения инициализатора или из возвращаемого значения геттера.
Геттеры и сеттеры необязательно определять именно для свойств внутри класса, они могут также применяться к переменным верхнего уровня.
Сеттер
Сеттер определяет логику установки значения переменной. Он определяется с помощью слова set . Например, у нас есть переменная age , которая хранит возраст пользователя и представляет числовое значение.
Но теоретически мы можем установить любой возраст: 2, 6, -200, 100500. И не все эти значения будут корректными. Например, у человека не может быть отрицательного возраста. И для проверки входных значений можно использовать сеттер:
var age: Int = 18 set(value)< if((value>0) and (value <110)) field = value >fun main() < println(age) // 18 age = 45 println(age) // 45 age = -345 println(age) // 45 >
Блок set определяется сразу после свойства, к которому оно относится — в данном случае после свойства age . При этом блок set фактически представляет собой функцию, которая принимает один параметр — value, через этот параметр передается устанавливаемое значение. Например, в выражении age = 45 число 45 и будет представлять тот объект, который будет храниться в value.
В блоке set проверяем, входит ли устанавливаемое значение в диапазон допустимых значений. Если входит, то есть если значение корректно, то передаем его объекту field . Если значение некорректно, то свойство просто сохраняет свое предыдущее значение.
Идентификатор field представляет автоматически генерируемое поле, которое непосредственно хранит значение свойства. То есть свойства фактически представляют надстройку над полями, но напрямую в классе мы не можем определять поля, мы можем работать только со свойствами. Стоит отметить, что к полю через идентификатор field можно обратиться только в геттере или в сеттере, и в каждом конкретном свойстве можно обращаться только к своему полю.
В функции main при втором обращении к сеттеру ( age = -345 ) можно заметить, что значение свойства age не изменилось. Так как новое значение -345 не входит в диапазон от 0 до 110.
геттер
Геттер управляет получением значения свойства и определяется с помощью ключевого слова get :
var age: Int = 18 set(value)< if((value>0) and (value <110)) field = value >get() = field
Справа от выражения get() через знак равно указывается возвращаемое значение. В данном случае возвращается значения поля field , которое хранит значение свойства name. Хотя в таком геттер большого смысла нет, поскольку получить подобное значение мы можем и без геттера.
Если геттер должен содержать больше инструкций, то геттер можно оформить в блок с кодом внутри фигурных скобок:
var age: Int = 18 set(value)< println("Call setter") if((value>0) and (value <110)) field = value >get()
Если геттер оформлен в блок кода, то для возвращения значения необходимо использовать оператор return . И, таким образом, каждый раз, когда мы будем получать значение переменной age (например, в случае с вызовом println(age) ), будет срабатывать геттер, когда возвращает значение. Например:
Консольный вывод программы:
Call getter 18 Call setter Call getter 45
Использование геттеров и сеттеров в классах
Хотя геттеры и сеттеры могут использоваться к глобальным переменным, как правило, они применяются для опосредования доступа к свойствам класса.
fun main() < val bob: Person = Person("Bob") bob.age = 25 // вызываем сеттер println(bob.age) // 25 bob.age = -8 // вызываем сеттер println(bob.age) // 25 >class Person(val name: String)< var age: Int = 1 set(value)< if((value>0) and (value <110)) field = value >>
При втором обращении к сеттеру ( bob.age = -8 ) можно заметить, что значение свойства age не изменилось. Так как новое значение -8 не входит в диапазон от 0 до 110.
Вычисляемый геттер
Геттер может возвращать вычисляемые значения, которые могут задействовать несколько свойств:
fun main() < val tom = Person("Tom", "Smith") println(tom.fullname) // Tom Smith tom.lastname = "Simpson" println(tom.fullname) // Tom Simpson >class Person(var firstname: String, var lastname: String)
Здесь свойство fullname определяет блок get, который возвращает полное имя пользователя, созданное на основе его свойств firstname и lastname. При этом значение самого свойства fullname напрямую мы изменить не можем — оно определено доступно только для чтения. Однако если изменятся значения составляющих его свойств — firstname и lastname, то также изменится значение, возвращаемое из fullname.
Использование полей для хранения значений
Выше уже рассматривалось, что с помощью специального поля field в сеттере и геттере можно обращаться к непосредственному значению свойства, которое хранится в специальном поле. Однако мы сами можем явным образом определить подобное поле. Нередко это приватное поле:
Можно использовать одновременно и геттер, и сеттер:
fun main() < val tom = Person("Tom") println(tom.age) // 1 tom.age = 37 println(tom.age) // 37 tom.age = 156 println(tom.age) // 37 >class Person(val name: String) < private var _age = 1 var age: Int set(value)< if((value >0) and (value < 110)) _age = value >get()= _age >
Здесь для свойства age добавлены геттер и сеттер, которые фактически являются надстройкой над полей _age , которое собственно хранит значение.