Модификаторы доступа в Swift

Уровни доступа делают код безопасным и разделенным, уменьшают случайные ошибки.

Поможем в Telegram-чате для iOS разработчиков

Уровни доступа определяют доступность объектов и методов. Если объект закрыт уровнем доступа, то по ошибке обратиться к нему не получится, он просто не будет доступен. Конечно, можно игнорировать уровни доступа, но это снизит безопасность кода. Инкапсюлированный код показывает, какая часть кода является внутренней реализацией. Это критично для команд, где каждый работает над частью проекта.

В Swift эти ключевые слова обозначают уровни доступа:

  1. public
  2. internal
  3. fileprivate
  4. private
  5. open

Уровни доступа можно назначать свойствам, структурам, классам, перечислениям и модулям. Указывайте ключевые слова перед объявлением. Далее по тексту я буду использовать слово «модули». Модулем может быть приложение, библиотека или таргет.

 Про уровни доступа в Swift
Про уровни доступа в Swift

internal

Внутренний уровень стоит по умолчанию для свойств и методов и предоставляет доступ внутри модуля. Явно указывать internal не требуется.

Эти записи равнозначны:

var number = 3

internal var number = 3

Доступ к объектам с internal нельзя получить из другого модуля:

 Объекты классов A, B и C можно создать в новом файле исходного модуля, но нельзя использовать в другом модуле
Объекты классов A`, B и C` можно создать в новом файле исходного модуля, но нельзя использовать в другом модуле

public

Обычно его используют для фреймворков. Модули имеют доступ к публичным объектам других модулей.

За пределами исходного модуля public-классы не могут быть суперклассами, а их свойства и методы нельзя переопределять
 Классы A, B и C не могут быть суперклассами. Их объекты можно создать в новом файле исходного и другого модуля, но за пределами исходного нельзя переопределять свойства и методы
Классы A`, B и C` не могут быть суперклассами. Их объекты можно создать в новом файле исходного и другого модуля, но за пределами исходного нельзя переопределять свойства и методы

open

Похож на public - разрешает доступ из других модулей. Используется только для классов, их свойств и методов.

Как в определяющем, так и в импортирующем модуле open-классы могут быть суперклассами, а их свойства и методы могут переопределяться подклассами
 Объекты классов A, B и C можно создать как в новом файле исходного модуля, так и в другом модуле
Объекты классов A`, B и C` можно создать как в новом файле исходного модуля, так и в другом модуле

private

Ограничивает доступ к свойствам и методам внутри структур, классов и перечислений. private — самый строгий уровень, он скрывает вспомогательную логику.

 prop1 может быть использован в другом файле исходного модуля, а private prop2 только в классе, в котором его создали
prop1 может быть использован в другом файле исходного модуля, а private prop2 только в классе, в котором его создали

Для свойств

private-свойства читаются и записываются только в их структурах и классах.

Давайте напишем игру, где нужно дать правильный ответ. Создадим структуру Test с вопросом и ответом. Ответ будем сравнивать с ответом пользователя.

struct Test {

    let question = "Столица Перу?"
    let answer = "Лима"
}

Создадим экземпляр Test с именем test и распечатаем вопрос:

let test = Test()
print(test.question) // Столица Перу?

Мы знаем вопрос и знаем, как посмотреть ответ:

print(test.answer) // Лима

У игрока не должно быть доступа к ответу — укажем уровень private для свойства answer.

struct Test {

    let question = "Столица Перу?"
    private let answer = "Лима"
}

Распечатаем вывод:

print(test.question) // Столица Перу?
print(test.answer) // Ошибка: 'answer' is inaccessible due to 'private' protection level

Мы получили ошибку: answer недоступен из-за уровня доступа private. Поведение private-свойств в классах аналогично. Прочесть свойство answer могут только члены структуры Test. Создадим метод showAnswer для вывода ответа на экран:

struct Test {

    // ...

    func showAnswer() {
        print(answer)
    }
}

Проверяем:

test.showAnswer() // Лима

Для методов

Когда работаете с конфиденциальными данными, указывайте методам private, чтобы спрятать реализацию. Создадим переменные gamerAnswer и result типа String с пустыми начальными значениями. result сделаем private:
struct Test {

    let question = "Столица Перу?"
    private let answer = "Лима"
    var gamerAnswer = ""
    private var result = ""

    // ...
}

Понадобятся два метода:

  1. compareAnswer() - сравнивает ответ игрока с правильным ответом, перезаписывает значение свойства result
  2. getResult() - выводит значение result на экран

У нас будет доступ к getResult() снаружи структуры Test, а вот compareAnswer() сделаем private.

struct Test {

    // ...
    
    private mutating func compareAnswer() {
        switch gamerAnswer {
        case "":
            result = "Вы не ответили на вопрос".
        case answer:
            result = "Ответ верный!"
        default:
            result = "Ответ неверный".
        }
    }
    
    mutating func getResult() {
        compareAnswer()
        print(result)
    }
}

Играем!

var test = Test()
print(test.question) // "Столица Перу?"
test.gamerAnswer = "Лима"
test.getResult() // "Ответ верный!"

fileprivate

Похож на private. Доступ к объектам этого уровня есть только у объектов из того же файла. fileprivate пригодится, когда нам нужны дополнительные объекты или вычисления в рамках одного файла.

 prop1 может быть использован в другом файле исходного модуля, а fileprivate prop2 только в файле, в котором его создали
prop1 может быть использован в другом файле исходного модуля, а fileprivate prop2 только в файле, в котором его создали

Отличие от private

Создадим два файла: File1.swift и File2.swift. В первом файле структуры Constants и PrinterConstants:
struct Constants {

    static let decade = 10
    static let exp = 2.72
}

struct PrinterConstants {

    func printConstants() {
        print(Constants.decade)
        print(Constants.exp)
    }
}

В File2.swift структура PrinterConstantsFromOuterFile:

struct PrinterConstantsFromOuterFile {

    func printConstants() {
        print(Constants.decade)
        print(Constants.exp)
    }
}

static постоянные структуры Constants имеют уровень internal. Это позволяет другим структурам из обоих файлов обращаться к ним. Укажем private свойству Constant.exp.

struct Constants {

    // ...
    
    private static let exp = 2.72
}
Теперь структуры PrinterConstants и PrinterConstantsFromOuterFile не могут обращаться к свойству Constant.exp. Заменим private на fileprivate:
struct Constants {

    // ...
    
    fileprivate static let exp = 2.72
}

У структуры PrinterConstantsFromOuterFile нет доступа к свойству Constatnts.exp, а у PrinterConstants есть. Исправим ошибку. Удалим строку print(Constants.exp) из структуры PrinterConstantsFromOuterFile.

struct PrinterConstantsFromOuterFile {

    func printConstants() {
        print(Constants.decade)
    }
}

Вычисляемые свойства

Вычисляемые свойства используют другие свойства, чтобы вернуть значение. Такие свойства принято делать private и public private уровней.

Read-only

Вычисляемым read-only-свойством считается только свойство с getter.

Создадим структуру HappyMultiply. Свойство multipliedHappyLevel рассчитаем на основе private свойства happyLevel, чтобы скрыть вычисления.

struct HappyMultiply {

    private var happyLevel: UInt
 
    var multipliedHappyLevel: UInt {
        get {
            return happyLevel != 0 ? happyLevel * 10 : 10
        }
    }
}

Private Setter

Приватный setter используют для ограничения доступа к записи за пределами структуры (класса). Для объявления приватного сеттера используем совместно ключевые слова private и set. Создадим структуру Vehicle. Укажем свойству numberOfWheels приватный сеттер:

struct Vehicle {

    private(set) var numberOfWheels : UInt
}

Public Private Setter

Можно переписать структуру Vehicle иначе.

struct Vehicle {

    public private(set) var numberOfWheels : UInt = 3
}

var kidBike = Vehicle()
print(kidBike.numberOfWheels) // 3
kidBike.numberOfWheels = 2 // Ошибка: cannot assign to property: 'numberOfWheels' setter is inaccessible

Getter имеет уровень доступа public, а setter - private.

Модули и фреймворки

Мы хотим создать модуль Tools с письменными принадлежностями. Создадим internal класс WritingTool со свойствами name, inscription и методом write(word: String).

  1. name - постоянная типа String, название инструмента
  2. inscription - переменная типа String с пустым начальным значением, надпись
  3. write(word: String) добавляет word к inscription
class WritingTool {

    let name: String
    var inscription = ""
    
    init(name: String) {
        self.name = name
    }
    
    func write(word: String) {
        inscription += word
    }
}

В рамках модуля в любом месте проекта мы создаём подкласс на его основе.

class Pencil: WritingTool {

    func clear() {
        inscription = ""
    }
}

Создать экземпляр класса Pencil можно в любом месте модуля.

let redPencil = Pencil(name: "red pencil")
redPencil.write(word: "writing by pencil")
print(redPencil.inscription) // "writing by pencil"
redPencil.clear()
print(redPencil.inscription) // ""
Классы WritingTool и Pencil доступны только внутри нашего модуля из-за internal-уровня. Для нашей задачи internal не подходит

Изменим уровень класса Pencil на public.

public class Pencil: WritingTool {}

Получаем ошибку: «Сlass cannot be declared public because its superclass is internal».

Уровень подкласса не должен быть мягче уровня его суперкласса

Изменим уровень класса WritingTool на public.

public class WritingTool {}
Теперь можно импортировать модуль в другие проекты и использовать классы WritingTool и Pencil.
import Tools

let redPencil = Pencil(name: "red pencil")
redPencil.write(word: "writing by pencil")
print(redPencil.inscription) // "writing by pencil"
redPencil.clear()
print(redPencil.inscription) // ""

В новом проекте мы хотим создать класс Pen, наследующийся от WritingTool.

public не позволяет классам WritingTool и Pencil быть суперклассами за пределами модуля Tools. Нужен другой уровень

В модуле Tools изменим уровень класса WritingTool на open.

open class WritingTool {}

В новом проекте теперь можно создать класс Pen: WritingTool.

import Tools

class Pen: WritingTool {

    var inkColor: CGColor = .black
    
    func changeInk(color: CGColor) {
        inkColor = color
    }
}

Класс Pencil мы оставили с уровнем public. Он может использоваться в новом проекте, но не может быть в нём суперклассом.

import Tools

class Pen: WritingTool {}

let greenPencil = Pencil(name: "green pencil")
let pen = Pen(name: "pen")
Свойства и методы класса WritingTool (open уровень) могут быть переопределены классами Pen и Pencil. Свойства и методы класса Pencil (public уровень) могут быть переопределены только его подклассами в модуле Tools.

Кортежи

Уровень доступа кортежа вычисляется на основе уровней входящих в него типов и получает самый строгий уровень из всех входящих в него.

Рассмотрим пример:

struct A {
    
    let one = 1
    private let two = 2
    var toupleOneTwo: (Int, Int)
    
    init () {
        self.toupleOneTwo = (one, two)
    }
}

let a = A()
a.one // 1
a.toupleOneTwo // (.0 1, .1 2)
В структуре A` свойство one имеет уровень internal, а свойство two - private. Кортеж toupleOneTwo доступен снаружи структуры A. Для toupleOneTwo мы указали тип (Int, Int), и передали значения свойств one и two, а не попытались обратиться снаружи к private свойству two`.

Перейдём к определению Int:

@frozen public struct Int : FixedWidthInteger, SignedInteger { 

    // ... 
}
Из этого определения следует, что кортеж toupleOneTwo имеет уровень public. Тогда он должен быть доступен вне определяющего модуля. Но сама структура A`, как и её экземпляр a, имеет уровень internal, из-за чего она не будет доступна в другом модуле, как и свойство toupleOneTwo`.

Другой пример. Создадим две структуры: Letters - fileprivate, Numbers- private.

fileprivate struct Letters {
	
    var userLetter: Character
}

private struct Numbers {
	
    var userNumber: UInt8
}

Теперь напишем internal структуру Info, свойство userInfo которой имеет тип (Letters, Numbers).

struct Info {
	
    var userInfo: (Letters, Numbers)
}
Мы получили ошибку "property must be declared fileprivate because its type uses a private type". В данном случае для файла, в котором мы объявили структуры Letters и Numbers, их уровни fileprivate и private равнозначны - предоставляют доступ только внутри файла. Поэтому userInfo не получает уровень private автоматически, хоть он и строже fileprivate. Мы можем использовать любой из этих двух уровней для userInfo.
struct Info {
	
    fileprivate var userInfo: (Letters, Numbers)
}

Теперь можно создать экземпляр структуры Info.

let info = Info(userInfo: (Letters(userLetter: "A"), Numbers(userNumber: 1)))

Изменим fileprivate на private.

struct Info {
	
    private var userInfo: (Letters, Numbers)
}

Получаем ошибку «'Info' initializer is inaccessible due to 'private' protection level». Мы не можем создать экземпляр этой структуры из-за уровня private свойства userInfo. Типы, входящие в кортеж, позволяют нам сделать это свойство private, но использовать его мы не можем.

Поправить или дополнить статью через Pull Request