Зачем прятать часть кода от самого себя
Представьте банкомат. Клиент видит только кнопки «снять» и «внести», а провода, счётчики и защёлки внутри корпуса ему недоступны — и это не каприз инженеров, а способ не дать никому руками испортить баланс в обход правил. В коде работает та же идея: часть свойств и методов типа должна быть видна и доступна всем, а часть — только самому типу, чтобы никто извне не мог поменять её в обход нужной логики.
За это в Swift отвечают модификаторы доступа — ключевые слова перед объявлением свойства, метода, типа или функции, которые ограничивают, откуда к ним можно обратиться.
Что такое модуль
Многие уровни доступа определяются через понятие модуля. Модуль — это единица кода, которая компилируется как одно целое: ваше приложение, отдельная библиотека или пакет, подключаемый через Swift Package Manager. Всё, что вы пишете в рамках одного курса задач и на сайте, находится в одном модуле, поэтому разница между некоторыми уровнями доступа станет заметна только тогда, когда вы начнёте собирать код из нескольких модулей.
Шесть уровней: от самого закрытого до самого открытого
Уровни доступа перечисляют от строгого к свободному:
private // видно только внутри этого же объявления
fileprivate // видно в пределах всего файла
internal // видно в пределах модуля — уровень по умолчанию
package // видно в пределах пакета SPM, даже нескольким модулям
public // видно за пределами модуля, но нельзя переопределять снаружи
open // видно за пределами модуля и можно наследовать/переопределять
private — самый строгий уровень. Он открывает доступ только внутри того же объявления, в котором свойство или метод описаны: если это struct или class, то только методам и свойствам этого же типа.
struct Safe {
private var code: Int = 1234
func isCorrect(_ guess: Int) -> Bool {
return guess == code
}
}
let safe = Safe()
print(safe.isCorrect(1234))
Метод isCorrect объявлен внутри Safe, поэтому ему разрешено читать code напрямую. А вот строка вида safe.code снаружи структуры компилятор отклонит: свойство private, значит наружу его не видно вообще, только через методы вроде isCorrect.
fileprivate чуть свободнее: доступ открыт всему, что написано в том же файле, даже другим типам. Он пригождается, когда два типа в одном файле тесно сотрудничают и должны видеть внутренности друг друга, но снаружи файла это всё равно должно быть скрыто.
internal — уровень по умолчанию: если модификатор вообще не написан, действует именно он. Он открывает доступ в пределах всего модуля, но не дальше. Именно поэтому весь код, который вы пишете в задачах на этой платформе, спокойно видит друг друга без единого модификатора — вы работаете в одном модуле.
package нужен реже: он открывает доступ нескольким модулям, которые физически собираются вместе как один пакет SPM, но при этом остаются закрытыми снаружи пакета совсем — ни для кого извне.
public и open оба открывают доступ за пределы модуля — например, для библиотеки, которую подключают другие проекты. Разница между ними — в наследовании: класс с модификатором public нельзя унаследовать и переопределить его методы из другого модуля, а с open — можно. Для структур и перечислений разницы между public и open нет вовсе, потому что у них нет наследования.
private(set): читать может любой, менять — только сам тип
Часто нужна не крайность («видно всем» или «не видно никому»), а середина: пусть значение свойства читают откуда угодно, но меняют — только методы самого типа. Для этого перед var ставят private(set) — модификатор доступа применяется только к сеттеру, а не ко всему свойству.
struct Counter {
private(set) var value: Int = 0
mutating func increment() {
value += 1
}
}
var counter = Counter()
counter.increment()
counter.increment()
print(counter.value)
Строка counter.value читает свойство — это разрешено. А вот строка вида counter.value = 10 снаружи структуры не скомпилируется: менять значение можно только изнутри, например через метод increment. Так тип сам гарантирует, что значение растёт только на единицу за раз и никогда не появится «из ниоткуда».
Вместо private(set) можно указать любой другой уровень для сеттера — например, internal private(set) var value: Int означало бы «читать можно всем в модуле, менять — только внутри типа». Но чаще всего свойство и так по умолчанию internal, и достаточно написать просто private(set).
Частые ошибки
- Делать вообще всё
private«на всякий случай» — тогда тип становится бесполезным снаружи: у него не остаётся ни одного способа с ним взаимодействовать. - Путать
privateиfileprivate:privateзакрывает доступ даже другому типу в том же файле, если это не расширение того же самого типа, аfileprivate— нет. - Забывать, что
internal— это уровень по умолчанию, и писать его явно там, где он не нужен: код от этого не станет безопаснее, просто добавится лишнее слово. - Ставить
private(set)и одновременно менять свойство напрямую из того же метода без всякой проверки — тогда инкапсуляция есть только на бумаге, а реальной защиты от некорректных значений в методе нет. - Ожидать, что
publicпозволит наследовать класс из другого модуля — для этого нужен именноopen.
Резюме
- Модификаторы доступа ограничивают, откуда видно свойство, метод, тип или функцию: от
private(только внутри объявления) доopen(видно и наследуется за пределами модуля). internal— уровень по умолчанию, действует, если модификатор не написан вообще.fileprivateоткрывает доступ всему файлу,package— модулям внутри одного пакета SPM.publicоткрывает доступ за пределы модуля без права переопределения,open— с правом наследования и переопределения.private(set)разрешает свободное чтение свойства, но ограничивает изменение только кодом внутри самого типа — так тип защищает своё состояние от некорректных изменений снаружи.
Проверьте себя
4 вопроса
Кошелёк с защищённым балансом
Реализуйте тип Wallet — электронный кошелёк с балансом, который нельзя изменить напрямую снаружи, только через методы самого кошелька.
У структуры уже объявлено свойство balance: Int с модификатором private(set): читать его можно откуда угодно, а менять разрешено только коду внутри самой структуры.
Реализуйте метод mutating func deposit(_ amount: Int): если amount больше нуля, увеличьте balance на эту сумму; если amount меньше или равен нулю — ничего не делайте.
Реализуйте метод mutating func withdraw(_ amount: Int) -> Bool: если amount больше нуля и не превышает текущий balance, уменьшите balance на эту сумму и верните true. В остальных случаях (сумма не больше нуля или превышает баланс) баланс менять нельзя, метод должен вернуть false.