Public get private set в Kotlin: ловушки и решения

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Public get, private set в Kotlin: как не наступить на грабли (и что делать, если уже наступили)

Привет, коллега! Если ты когда-нибудь гуглил "Kotlin public get private set var", то ты не одинок. Этот запрос — настоящий хит на Stack Overflow, и тысячи разработчиков (включая меня в своё время) бились головой о клавиатуру, пытаясь понять, почему их код не работает.

Казалось бы, что сложного? Хочу, чтобы свойство было доступно всем (public get), но менять его мог только сам класс (private set). В Java это была рутина: объявляешь поле private, пишешь геттер и сеттер, а сеттер делаешь приватным. В Kotlin же всё должно быть проще, но...

Спойлер: синтаксис есть, но он не всегда работает так, как ты ожидаешь. В этой статье мы разберём топ ошибок, которые допускают даже опытные разработчики, и покажем, как их избежать. Поехали!

Анатомия свойства: var, val, get и set

Для начала — быстрый ликбез. В Kotlin свойства (properties) — это не просто поля. Это полноценные элементы, которые могут содержать геттеры и сеттеры. Вот базовая структура:

var имя_свойства: Тип = значение_по_умолчанию
    get() = field
    set(value) { field = value }

Если ты используешь var, Kotlin автоматически генерирует геттер и сеттер. Но ты можешь переопределить их поведение. А ещё — изменить их видимость (visibility modifiers: public, private, internal, protected).

И вот тут начинается самое интересное. Чтобы сделать геттер публичным, а сеттер приватным, нужно написать:

var name: String = "default"
    private set

Всё? Почти. Но есть нюансы, которые ломают этот механизм.

Ошибка №1: Забыть про конструктор

Самая частая проблема — попытка объявить public get private set var в конструкторе класса. Например:

// ❌ Ошибка: так нельзя
class User(public get private set var name: String)

Kotlin выдаст ошибку компиляции. Почему? Потому что параметры конструктора — это не свойства. Это просто параметры. Если ты хочешь, чтобы свойство было с изменённой видимостью сеттера, его нужно объявлять в теле класса.

Как правильно:

class User(name: String) {
    var name: String = name
        private set
}

Или, если используешь data class, придётся выкручиваться:

data class User(private val _name: String) {
    var name: String = _name
        private set
}

Да, data class не позволяет менять видимость сеттера напрямую для свойств из primary constructor. Придётся использовать backing field.

Ошибка №2: Путаница с backing field

Backing field — это скрытое поле, которое Kotlin автоматически генерирует для свойства, если ты используешь field в геттере или сеттере. Но если ты переопределяешь геттер или сеттер без обращения к field, backing field не создаётся.

Пример проблемы:

var counter: Int = 0
    private set
    get() = field * 2 // ❌ Ошибка: field не определён

Нет, это не ошибка компиляции. Но если ты напишешь:

var counter: Int = 0
    private set
    get() = 42 // всё ок, но field не используется

В этом случае backing field не будет создан. И если ты попытаешься обратиться к field в сеттере, получишь ошибку. Хорошая новость: если ты используешь private set, backing field почти всегда создаётся автоматически, потому что сеттер по умолчанию использует field. Но если ты переопределишь сеттер без field — будь готов к сюрпризам.

Совет: всегда явно используй field в геттере и сеттере, если хочешь работать с backing field. И помни: если свойство не имеет backing field, ты не сможешь использовать private set в том виде, к которому привык.

Ошибка №3: Сеттер не вызывается при прямой записи в поле

Вот классический сценарий: ты объявил свойство с приватным сеттером, но внутри класса случайно обращаешься к полю напрямую через this или field. Например:

class BankAccount {
    var balance: Double = 0.0
        private set

    fun deposit(amount: Double) {
        // ❌ Ошибка: обращаемся к полю, а не к сеттеру
        // balance = balance + amount // сеттер приватный, но внутри класса он доступен
        // Но если написать так:
        field = balance + amount // ❌ Ошибка! field доступен только в сеттере или геттере
    }
}

Внутри класса ты можешь обращаться к свойству напрямую — сеттер приватный, но это не мешает изменять значение из методов того же класса. Однако если ты попытаешься использовать field вне геттера/сеттера, компилятор ругнётся.

Как правильно:

class BankAccount {
    var balance: Double = 0.0
        private set

    fun deposit(amount: Double) {
        balance += amount // ✅ работает, сеттер вызывается, но он приватный
    }
}

Но будь осторожен: если ты переопределишь сеттер, он будет вызван. А если нет — значение просто запишется. Всё логично, но новички часто путаются.

Ошибка №4: Иммутабельность через val

Иногда разработчики пытаются сделать public get private set для val. Но val — это read-only свойство. У него нет сеттера. Вообще. Поэтому написать val name: String private set нельзя. Компилятор скажет: "Сеттер не поддерживается для val".

Если тебе нужно, чтобы свойство нельзя было изменить снаружи, но можно было внутри — используй var с приватным сеттером. Если свойство вообще не должно меняться — val.

Лайфхак: можно комбинировать:

class Config {
    var loaded: Boolean = false
        private set

    fun load() {
        loaded = true
    }
}

Ошибка №5: Забыть про интерфейсы и наследование

Если ты объявляешь свойство с приватным сеттером в открытом (open) классе или интерфейсе, будь готов к ограничениям. Например:

open class Base {
    open var value: String = ""
        private set // ❌ Ошибка: private set несовместим с open
}

Kotlin не позволяет переопределить свойство, если его сеттер приватный. Почему? Потому что дочерний класс не сможет изменить видимость сеттера. Придётся делать сеттер protected или public.

Решение: используй protected set, если хочешь, чтобы наследники

Блоги

Рекомендаций по книгам