Геттеры и сеттеры в 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 {
    step = gap
}

В классе может быть несколько блоков 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. Однако в таких случаях может быть проще отказаться от первичного конструктора и использовать только вторичные.

Практическая работа:

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

PDF-версия курса с ответами к практическим работам


Введение в объектно-ориентированное программирование на Kotlin




Все разделы сайта