Основы Swift / 18.3. Контроль доступа


Видео


Контроль доступа
Контроль доступа ограничивает доступ к частям Вашего кода из кода в других файлах или модулей. Эта возможность позволяет Вам скрыть детали реализации Вашего кода и выбрать предпочтительный интерфейс, через который Ваш код может быть использован.

Вы можете назначить конкретные уровни доступа для отдельных типов (классов, структур и перечислений), ровно как для свойств, методов, конструкторов и сабскриптов, принадлежащих этих типам. Протоколы могут быть ограничены конкретным контекстом, как и глобальные константы, переменные и функции.

В дополнение к различным уровням контроля доступа Swift уменьшает необходимость явно указывать уровни доступа, предоставляя их для типичных сценариев. Если Вы пишите одноцелевое приложение, Вам может быть не нужно указывать явные уровни доступа в целом.

Различные аспекты Вашего кода, которые могут использоваться в контроле доступа, (свойства, типы, функции и так далее) будут называться в главе ниже как "сущности", для краткости.
Модули и исходные файлы
Модель контроля доступа в Swift строится на концепции модулей и исходных файлов. Модуль - это единица поставки кода - фреймворк или приложение, которые собираются и поставляются как общая единица и которые могут быть импортированы другим модулем с помощью ключевого слова import.

Каждая цель сборки (как контейнер приложения или фреймворк) в Xcode считается отдельным модулем в Swift. Если Вы объедините вместе аспекты кода Вашего приложения в качестве отдельного фреймворка (возможно, для инкапсуляции и реиспользования этого кода в нескольких приложениях), тогда всё, что Вы определите внутри этого фреймворка, будет частью отдельного модуля, когда оно будет импортироваться и использоваться внутри приложения или когда оно будет использоваться внутри другого фреймворка.

Исходный файл - это отдельный файл исходного кода Swift внутри модуля (в действительности, отдельный файл внутри приложения или фреймворка). Хотя и принято определять отдельные типы в разных исходных файлах, один исходный файл может содержать определения для множества типов, функций и так далее.
Уровни доступа
Swift предоставляет пять разных уровней доступа для элементов в Вашем коде. Эти уровни доступа относительны касательно исходного файла, в котором определена сущность, а также модуля, которому принадлежит исходный файл.
  • Открытый и публичный доступ позволяет сущностям быть использованными внутри любого исходного файла из их определяющего модуля, а также в исходном файле из другого модуля, который импортирует определяющий модуль. Вы будете обычно использовать открытый или публичный доступ, когда определяете публичный интерфейс для фреймворка. Различие между открытым и публичным интерфейсом описано ниже.
  • Внутренний доступ позволяет сущностям использовать изнутри любого исходного файла в определяющем модуле, но не в любом исходном фалде вне этого модуля. Вы обычно используете внутренний доступ, когда определяете структуру приложения или фреймворка.
  • Приватный доступ файла ограничивает использование сущности в своём собственном определяющем исходном файле. Используйте доступ приватный для файла для сокрытия реализации отдельной части функциональности, кода эти детали используются во всём файле.
  • Приватный доступ ограничивает использование сущности окружающим объявлением и расширениями этой декларации, которые находятся в том же файле. Используйте приватный доступ для сокрытия деталей реализации отдельной части функциональности, когда эти детали используются только в одном объявлении.
Открытый доступ - это наивысший (наименее ограничивающий) уровень доступа, и приватный доступ - наименьший (наиболее ограничивающий) уровень доступа.

Открытый доступ применяется только к классам и членам классов, и он отличается от публичного следующим:
  • Классы с публичным доступом или более строгим уровнем доступа могут быть унаследованы только внутри модуля, в котором они определены.
  • Члены классов с публичным доступом или более строгим уровнем доступа, могут быть перезаписаны только подклассами изнутри модуля их определения.
  • Открытые классы могут быть унаследованы внутри модуля их определения, а также любым модулем, импортирующим их определяющий модуль.
  • Члены открытых классов могут быть перезаписаны подклассами внутри модуля их определения, а также любым модулем, импортирующим их определяющий модуль.
Пометив класс как открытый, Вы явно укажите, что Вы согласны с тем, что код из других модулей может использовать этот класс в качестве суперкласса и что Вы спроектировали код этого класса соответствующим образом.
Основополагающий принцип уровней доступа
Уровни доступа в Swift следуют руководящему принципу: ни одна сущность не может быть определена в терминах другой сущности, которая имеет меньший (более ограничивающий) уровень доступа.

Например:
1. Публичная переменная не может быть определена как имеющая внутренний, приватный для файла, приватный уровни доступа, так как тип может не быть определён везде, где эта публичная переменная используется.
2. Функция не может иметь более высокий уровень доступа, чем типы её параметров и возвращаемого значения, так как функция может быть использована в ситуациях, где её составляющие типы не доступны в окружающем коде.

Специфичное приложение этого руководящего принципа для разданных аспектов языка описаны детально ниже.
Уровни доступа по-умолчанию
Все сущности в Вашем коде (с несколькими спешными исключениями, как это будет описано позднее в этой главе) имеют уровни доступа внутренние, если Вы не укажите их явно. В результате в большинстве случаев Вам не нужно указывает явный уровень доступа в Вашем коде.
Уровни доступа для одноцелевых приложений
Когда Вы пишите простое одноцелевое приложение, код в Вашем приложении обычно содержится в самом приложении и ему не нужно быть доступным извне модуля приложения. Уровень доступа по-умолчанию удовлетворяет этому требованию. Следовательно, Вам не нужно явно указывать уровень доступа. Вы, однако, можете отметить некоторые части Вашего кода как доступные только в файле или приватными для того, чтобы скрыть детали их реализации от другого кода в модуле Вашего приложения.
Уровни доступа для целей юнит-тестирования
Когда Вы пишите приложение с целью для юнит-тестирования, то коду в Вашем приложении нужно быть доступны для модуля, чтобы быть протестированным. По умолчанию, только сущности, помеченные в качестве открытых или публичных доступны другим модулям. Однако, цель юнит-тестирования может получить доступ к любым внутренним сущностям, если Вы отметите объявление импорта для модуля продукта с помощью атрибута @testable и соберёте этот модуль продукта с включенным тестированием.
Уровни доступа для фреймворков
Когда Вы разрабатываете фреймворк, пометьте публичный интерфейс фреймворка в качестве открытого или публичного, так чтобы он мог быть виден и использован другими модулями, как например приложением, которое импортирует этот фреймворк. Этот публичный интерфейс является интерфейсом программирования приложения (или API) для этого фреймворка.

Любые внутренние детали реализации Вашего фреймворка всё ещё могут использовать дефолтный уровень доступа как внутренний или могут быть помечены как приватные/приватные-для-файла, если Вы хотите скрыть их от других частей внутреннего кода фреймворка. Вам нужно пометить сущность как открытую или публичную, только если Вы хотите сделать её частью API Вашего фреймворка.
Синтаксис контроля доступа
Определите уровни доступа для сущности, разместив один из следующих модификаторов перед представлением сущности: open, public, internal, fileprivate, private:
@{18.3\1}
public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}

public var somePublicVariable = 0
internal let someInternalConstant = 0
fileprivate func someFilePrivateFunction() {}
private func somePrivateFunction() {}
@{18.3\2}
Пока не указано иное, уровень доступа по-умолчанию является. Это означает, что SomeInternalClass и someInternalConstant могут быть записаны без явного модификатора уровня доступа и всё будут иметь внутренний уровень доступа:
class SomeInternalClass {}              // неявный internal
let someInternalConstant = 0            // неявный internal
Пользовательские типы
Если Вы хотите явно указать доступ для своего типа, то Вы можете сделать это в момент его определения. Затем новый тип может быть использован везде, где позволяет его уровень доступа. Например, если Вы определите класс с уровнем доступа только-для-файла, то этот класс может быть использован в качестве типа свойства или параметра функции или её возвращаемого значения в том же исходном файле, где этот класс определён.

Уровень контроля доступа типа также оказывает влияние на уровни доступа по-умолчанию для членов этого класса (его свойства, методы, инициализаторы и сабскрипты). Если Вам нужно определить уровень доступа типа как приватный или приватный для-файла, то таким же станет уровень доступа станет для его членов. Если Вы определите уровень доступа для типа как внутренний или публичный (или будете использовать дефолтный уровень доступа внутренний без явного указания уровня доступа), то дефолтные уровне доступа для членов типа будет внутренним.

Публичный тип по-умолчанию имеет внутренних членов, а не публичных. Если Вы захотите объявить члена типа в качестве публичного, Вы должны явно пометить его таковым. Это требование гарантирует, что публичный API для этого типа является именно тем, что Вы хотите опубликовать без раскрытия его внутренного устройства для типа в качестве публичного API по ошибке.
@{18.3\3\1}
public class SomePublicClass {
    // явный public class
    public var somePublicProperty = 0
    // явный public class member
    var someInternalProperty = 0
    // неявный internal class member
    fileprivate func someFilePrivateMethod() {}
    // явный file-private class member
    private func somePrivateMethod() {}
    // явный private class member
}

class SomeInternalClass {
    // неявный internal class
    var someInternalProperty = 0
    // неявный internal class member
    fileprivate func someFilePrivateMethod() {}
    // явный file-private class member
    private func somePrivateMethod() {}
    // явный private class member
}

fileprivate class SomeFilePrivateClass {
    // явный file-private class
    func someFilePrivateMethod() {}
    // неявный file-private class member
    private func somePrivateMethod() {}
    // явный private class member
}

private class SomePrivateClass {
    // явный private class
    func somePrivateMethod() {}
    //неявный private class member
}
Типы-кортежи
Уровень доступа для типа-кортежа есть наиболее ограничивающий тип из всех типов, используемых в этом кортеже. Например, Вы можете составить кортеж из двух различных типов, один с внутренним доступом, а другой - с приватным, то уровень всего составного типа кортежа будет приватным.

Типы-кортежи не имеют своего отдельного определения, как его имеют класс, структуры и перечисления, функции. Уровень доступа типа-кортежа автоматически выводится, когда тип-кортеж используется, и не может быть задан явно.
Функциональные типы
Уровень доступа для функции вычисляется как наиболее ограничивающий тип из типов параметров функции и возвращаемого значения. Вы должны указать уровень доступа функции явно как часть её определения, если вычисленный уровень доступа функции не совпадает с тем, что должен быть в контексте.
@{18.3\3\2}
Пример ниже определяет глобальную функцию с названием someFunction() без указания явного модификатора доступа для этой функции. Вы можете ожидать, что эта функция будет иметь внутренний уровень доступа, но это не тот случай. На самом деле функция someFunction() не скомпилируется в виде, в котором она написана ниже:
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}
Возвращаемый тип функции является кортежем из двух классов. Один из этих классов был определён как внутренний, а другой - как приватный. Следовательно, весь уровень доступа составного типа кортежа будет приватным (минимальный уровень доступа составляющих кортеж типов).
@{18.3\3\3}
Так как возвращаемый тип функции - приватный, то Вы должны пометить весь уровень доступа функции модификатором private для того, чтобы объявление функции было корректным:
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}
Некорректно пометить определение someFunction() модификаторами public или internal или использовать стандартное значение внутренного доступа, так как публичные или внутренние пользователи функции могут не иметь соответствующего доступ к приватному классу, используемому в возвращаемом типе функции.
Типы-перечисления
Отдельные кейсы перечисления автоматически получают тот же уровень доступа, что и перечисление, которое они принадлежат. Вы не можете явно указать различные уровни доступа для отдельных кейсов перечислений.
@{18.3\4}
В примере ниже перечисление CompassPoint имеет явный уровень доступа публичный. Кейсы перечисления north, south, east и west как следствие будут иметь публичный уровень доступа:
public enum CompassPoint {
    case north
    case south
    case east
    case west
}
Чистые и ассоциированные значения
Типы, используемые для любого чистого или ассоциированного значения в определении перечисления должны иметь уровень доступ, хотя бы настолько же высокий, как и уровень доступа перечисления. Вы не можете использовать тип private в качестве чистого значения для перечисления с уровнем доступа internal.
Вложенные типы
Вложенные типы, определённые внутри приватного типа имеют автоматически уровень доступа приватный. Вложенные типы, определённые внутри приватного-для-файла типа имеют автоматически уровень доступа приватный-для-файла. Вложенные типы, определённые внутри внутреннего или публичного типа имеют автоматически уровень доступа внутренний. Если Вы хотите, чтобы вложенный внутрь публичного типа тип был доступен публично, то Вы должны явно задать его как публичный.
Наследование
Вы можете унаследовать любой класс, который доступен в текущем контексте доступа. Подкласс не может иметь более высокий уровень доступа, чем его надкласс: например, Вы не можете написать публичный подкласс внутреннего надкласса.

В дополнение Вы можете перезаписать любой член класса (метод, свойство, конструктор или сабскрипт), который видим в соответствующем контексте доступа.
@{18.3\5}
Перезапись может сделать унаследованный член класса более доступным, чем его версия в надклассе. В примере ниже класс A - это публичный класс с методом, доступным только в файле, с названием someMethod(). Класс B - это подкласс класса A со сниженным уровнем доступа до внутреннего. Однако класс B предоставляет перезапись метода someMethod() с уровнем доступа внутренним, который выше чем у оригинальной реализации someMethod():
public class A {
    fileprivate func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {}
}
@{18.3\6}
Это даже верно для члена подкласса вызвать члена надкласса, который имеет меньшие привилегии доступа, чем член подкласса, если вызов члена надкласса происходит внутри контекста с разрешённым уровнем доступа (что значит, вызов внутри того же исходного фалда для надкласса с вызовом члена приватного-для-файла или внутри того же модуля, что надкласс для вызова члена внутреннего):
public class A {
    fileprivate func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {
        super.someMethod()
    }
}
Так как надкласс A и подкласс B определены в одном исходном файле, то реализации B корректно в методе someMethod() вызвать super.someMethod().
Константы, переменные, свойства и сабскрипты
Константа, переменная или свойство не могут быть более публичными, чем их тип. Некорректно писать публичное свойство с приватным типом, например. Аналогично, сабскрипт не может быть более публичным чем любой из типов его индексов или возвращаемого значения.

Если константа, переменная, свойство или сабскрипт используют приватный тип, то они так же должны быть помечены как private:
@{18.3\7}
private var privateInstance = SomePrivateClass()
Геттеры и сеттеры
Гетеры и сеттеры для констант, переменных, свойств и сабскриптов автоматически получают уровень доступа сущности, которой они принадлежат.

Вы можете дать сеттеру меньший уровень доступа, чем его корреспондирующему геттеру, чтобы ограничит пространство чтения-записи для этой переменой, свойства или сабскрипта. Вы присваиваете меньший уровень доступа с помощью fileprivate(set), private(set) или internal(set) перед вводными словами var или subscript.

Это правило применяется к хранимым свойства так же хорошо, как и к вычисляемым. Даже если Вы не напишите явных геттера/ и сеттера для хранимого свойства, Swift синтезирует для Вас неявные геттер и сеттер для предоставления доступа к хранилищу хранимого свойства. Используйте fileprivate(set), private(set) или internal(set) для изменения уровня доступа для этого синтезированного сеттера тем же способом, что и для явного сеттера в вычисляемом свойстве.
@{18.3\8\1}
Пример ниже определяет структуру с названием TrackedString, которая отслеживает количество раз, сколько было модифицировано строковое свойство:
struct TrackedString {
    private(set) var numberOfEdits = 0
    var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
}
Структура TrackedString определяет хранимое свойство с названием value, которое имеет изначальное значение "" (пустая строка). Структура также определяет хранимое целочисленное свойство с названием numberOfEdits, которое используется для отслеживания количества модификацией свойства value. Отслеживание этого изменения реализовано в наблюдателе свойства didSet значения value, который инкрементирует значение numberOfEdits всякий раз при присваивании нового значения свойству value.

Структура TrackedString и её свойство value не предоставляют явного модификатора уровня доступа, так что они получают уровень доступа по-умолчанию - внутренний. Однако доступ к numberOfEdits помечен как private(set) для указания на то, что геттер свойства всё ещё будет иметь внутренний уровень доступа, но свойство будет задаваемо только изнутри кода, который является частью структуры TrackedString. Это позволяет TrackedString изменять значение свойства numberOfEdits изнутри, но представить его вовне как свойство только-для-чтения.
@{18.3\8\2}
Если Вы создадите объект типа TrackedString и измените его строковое значение несколько раз, то Вы увидите, что значения свойства numberOfEdits обновится для совпадения с количеством изменений:
var stringToEdit = TrackedString()
stringToEdit.value = "This string will be tracked."
stringToEdit.value += " This edit will increment numberOfEdits."
stringToEdit.value += " So will this one."
print("The number of edits is \(stringToEdit.numberOfEdits)")
// Выведет "The number of edits is 3"
И хотя Вы можете запросить текущее значение свойства numberOfEdits изнутри другого исходного файла, Вы не можете изменить его оттуда. Это ограничение защищает детали реализации TrackedString от изменения значения отслеживания с собранием удобного доступа к аспектам её функциональности.
@{18.3\9}
Заметьте, что Вы можете присвоить явный уровень доступа и для геттера, и для сеттера, если понадобится. Пример ниже демонстрирует версию структуры TrackedString, в котором структура определена с явным публичным уровнем доступа. Члены структуры (включая свойство numberOfEdits) следовательно будут иметь внутренний уровень доступа по-умолчанию. Вы можете сделать геттер свойства numberOfEdits структуры публичным, а его сеттер приватным, объединив модификаторы public и private(set):
public struct TrackedString {
    public private(set) var numberOfEdits = 0
    public var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
    public init() {}
}
Инициализаторы
Кастомные конструкторы могут получить уровень доступа меньший или равный типу, который они инициализируют. Единственное исключение существует для требуемых конструкторов (как описано в {Требуемых инициализаторах}). Требуемый инициализатор должен иметь тот же уровень доступа, что и класс, которому он принадлежит.

Как и в случае с параметрами функций и методов типы параметров конструктора не могут быть более приватными, чем уровень доступа самого инициализатора.

Как описано в {Инициализаторах по-умолчанию}, Swift автоматически предоставляет дефолтный инициализатор без аргументов для любой структуры или базового класса, что предоставляют значения по-умолчанию для всех своих свойств и не содержат никаких кастомных инициализаторов.

Инициализатор по-умолчанию имеет тот же уровень доступа, что и тип, который он инициализирует, если этот тип не определён как public. Для типа определённого как public, дефолтный инициализатор определён как внутренний. Если Вы хотите, чтобы можно было инициализировать публично тип с конструктором без аргументов при его использовании в другом модуле, Вы должны явно задать публичный конструктор без аргументов в качестве определение типа.

Дефолтный почленный инициализатор для типа-структуры задаётся приватным, если любое из хранимых свойств структуры задаётся таковым. Аналогично, если любое из хранимых свойств структуры является приватным-для-файла, то инициализатор будет приватным-для-файла. В противном случае инициализатор будет иметь внутренний вровень доступа.

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

Уровень доступа каждого требования внутри протокола автоматически задаётся в тот же уровень, что и сам протокол. Вы не можете задать требованию протокола другой уровень доступа, отличный от поддерживаемого протоколом. Это гарантирует, что всегда требования протокола будут виды в любом типе, адаптирующем протокол.

Если Вы зададите публичный протокол, то требования протокола будут требовать публичный уровень доступа для реализуемых требований. Это поведение отлично от других типов, где определение публичного уровня доступа на тип делает все члены типа внутренними.

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

Тип может соответствовать протоколу с меньшим уровнем доступа, чем есть у типа. Например, Вы можете определить публичный тип, который может быть использован в других модулях, но адаптация внутреннего протокола может быть использована только внутри модуля определения протокола.

Контекст, в котором тип соответствует отдельному протоколу, это минимальный уровень из уровней доступа типа и протокола. Если тип публичен, но протокол, которому он соответствует, является внутренним, то соответствие типа протоколу так же внутреннее.

Когда Вы пишите или расширяете тип для адаптации протокола, Вы должны убедиться, что реализация типом каждого требования протокола имеет по крайней мере тот же уровень доступа, что адаптация типом протокола. Например, если публичный тип соответствует внутреннему протоколу, то реализация типом каждого требования протокола должна быть по крайней мере внутренней.

В Swift, как и в Objective-C, адаптация протокола глобальная: для типа невозможно удовлетворять протоколу двумя разными способами внутри одной программы.
Расширения
Вы можете расширить класс, структуру или перечисление в любом контексте доступ, в котором доступны класс, структура или перечисление. Любые члены типа, которые добавляются в расширении, имеют по-умолчанию тот же уровень доступа, что и члены типа, объявленные в оригинальном типе, который расширяется. Если Вы расширите публичный или внутренний тип, то любые новые члены типа, что Вы добавите, будет иметь значение доступа внутреннее. Если Вы расширите тип доступный-только-в-файле, то любые новые члены типа, что Вы добавите, будут иметь уровень доступа приватный-для-файла. Если Вы расширите тип приватный, то любые новые члены типа, что Вы добавите, будут иметь уровень доступа приватный.

С другой стороны, Вы можете пометить расширение явным модификатором доступа (например, private extension) для установки новых уровней доступа по-умолчанию для членов типа, определённых внутри расширения. Это новое значение по-умолчанию может быть перезаписано внутри расширения для отдельных членов типа.

Вы не можете предоставить явные модификаторы уровня-доступа для расширения, если Вы используете расширение для добавления соответствия протоколу. Вместо этого используется собственный уровень доступа протокола для предоставления уровней доступа по-умолчанию для каждой реализации требования протокола внутри расширения.

Расширения, что расположены в том же исходом файле, что класс, структура или перечисление, которые они расширяют, ведут себя так, будто код в этих расширениях был написан в качестве части изначального объявления типа. В результате Вы можете:
  • Объявить приватный член в оригинальной декларации и получить доступ к этому челну из расширений в том же файле.
  • Объявить приватный член в одном расширении и получить к нему доступ из другого расширения в том же файле.
  • Объявить приватный член в расширении и получить доступ к нему из изначального объявления в том же файле.
@{18.3\10\1}
Это поведение означает, что Вы можете использовать расширения тем же способом, что и обычно, независимо от того, имеют ли Ваши типы приватные сущности. Например, рассмотрим следующий простой протокол:
protocol SomeProtocol {
    func doSomething()
}
@{18.3\10\2}
Вы можете использовать расширение для добавления адаптации протокола например так:
struct SomeStruct {
    private var privateVariable = 12
}

extension SomeStruct: SomeProtocol {
    func doSomething() {
        print(privateVariable)
    }
}
Шаблоны
Уровень доступа к шаблонному типу или шаблонной функции выбирается как минимум из уровней доступа типа или функции и любого типа в ограничениях или параметров типа.
Ярлыки типов
Любые ярлыки типов, что Вы определяете, рассматриваются как отдельные типы в терминах контроля доступа. Ярлык типа может иметь уровень доступа, меньший или равный уровню доступа, который он оборачивает. Например, приватный ярлык типа может оборачивать любой тип, но публичный ярлык не может оборачивать внутренний, приватный-для-файла или приватный типы.

Это правило так же применяется для ярлыков типов для ассоциированных типов, используемых для адаптации протоколам.