$ sudo teach IT

Зачем прятать часть кода от самого себя

Представьте банкомат. Клиент видит только кнопки «снять» и «внести», а провода, счётчики и защёлки внутри корпуса ему недоступны — и это не каприз инженеров, а способ не дать никому руками испортить баланс в обход правил. В коде работает та же идея: часть свойств и методов типа должна быть видна и доступна всем, а часть — только самому типу, чтобы никто извне не мог поменять её в обход нужной логики.

За это в 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.