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, если хочешь, чтобы наследники