Контроль доступа
Контроль доступа ограничивает доступ к частям вашего кода из кода других исходных файлов и модулей. Эта особенность позволяет вам прятать детали реализации вашего кода и указывать на предпочтительный интерфейс, через который можно получить доступ к вашему коду.
Вы можете присвоить определенные уровни доступа как к индивидуальным типам (классы, структуры и перечисления), так и к свойствам, методам, инициализаторам и сабскриптам, принадлежащим этим типам. Протоколы могут быть ограничены в определенном контексте, так же как могут быть ограничены глобальные переменные или функции.
В дополнение к предложению контроля различных уровней доступа, Swift уменьшает необходимость указания явного уровня контроля доступа тем, что сам обеспечивает уровни доступа по умолчанию для типичных сценариев. И на самом деле, если вы пишете простое приложение, то вам может и не понадобится указывать явно уровень контроля доступа вообще.
Заметка
Различные аспекты вашего кода, к которым применим контроль уровня доступа (свойства, типы, функции и т.д.) будем называть “сущности”, для краткости.
Модули и исходные файлы
Модель контроля доступа Swift основывается на концепции модулей и исходных файлов.
Модуль представляет из себя единый блок распределения кода - фреймворк или приложение, которое построено и поставляется в качестве единого блока и которое может быть импортировано другим модулем с ключевым словом import.
Каждый таргет сборки (например, бандл приложения или фреймворк) в Xcode обрабатывается как отдельный модуль. Если вы объедините вместе аспекты кода вашего приложения в качестве отдельного фреймворка, то их возможно будет инкапсулировать и использовать заново во множестве других приложений. Таким образом, все, что вы определите в рамках этого фреймворка будет считаться частью отдельного модуля, когда это будет импортировано и использовано внутри приложения, или когда это будет использовано внутри другого фреймворка.
Исходный файл - исходный код файла в пределах одного модуля (в сущности это и есть один файл вашего приложения или фреймворка). Хотя в большинстве случаев определение типов происходит в отдельных исходных файлах, но фактически исходный файл может содержать определения множества различных типов, функций и т.д.
Уровни доступа
Swift предлагает пять различных уровней доступа для объектов вашего кода. Эти уровни доступа относительны исходному файлу, в котором определен объект, и так же они относительны модулю, которому принадлежит исходный файл:
- Открытый доступ и публичный доступ (open access и public access). Этот уровень доступа позволяет использовать объекты внутри любого исходного файла из определяющего их модуля и так же в любом исходном файле из другого модуля, который импортирует определяющий модуль. Вы обычно используете открытый и публичный доступы, когда указываете общий интерфейс фреймворка. Отличия между этими двумя уровнями доступа будет описаны ниже.
- Внутренний (internal access). Этот уровень доступа позволяет использовать объекты внутри любого исходного файла из их определяющего модуля, но не исходного файла не из этого модуля. Вы обычно указываете внутренний доступ , когда определяете внутреннюю структуру приложения или фреймворка.
- Файл-частный (file private). Этот уровень доступа позволяет использовать объект в пределах его исходного файла. Используйте файл-частный уровень доступа для того, чтобы спрятать детали реализации определенной части функциональности, когда эти части функциональности будут использоваться внутри другого файла.
- Частный (private). Этот уровень доступа позволяет использовать сущность только в пределах области ее реализации. Используйте частный доступ для того, чтобы спрятать детали реализации конкретной части функциональности, когда они используются только внутри области объявления.
Открытый доступ - самый высокий уровень доступа (наименее строгий), и частный уровень доступа является самым низким уровнем доступа (самый строгий).
Открытый доступ применяется только к классам и членам класса и отличается от public доступа следующим:
- Классы, с уровнем доступа public, могут наследоваться только в том модуле, в котором они определены.
- Члены класса с уровнем доступа public или с более строгим уровнем доступа могут быть переопределены подклассами только в том модуле, в котором они определены.
- Открытые классы могут наследоваться внутри модуля, в котором они определены и внутри модуля, который импортирует модуль, в котором они определены.
- Открытые члены класса могут переопределяться подклассами внутри модуля, в котором они были определены или внутри модуля, который импортирует модуль, в котором они были определены.
Обозначая класс через маркер open, явно свидетельствует о том, что вы рассмотрели влияние этого класса на код других модулей, использующих его в качестве суперкласса.
Руководящий принцип по выбору уровня доступа
Уровни доступа в Swift следуют общему руководящему принципу: никакой объект не может быть определен в пределах другого объекта, который имеет более низкий (более строгий) уровень доступа.
Например:
- Переменная с уровнем доступа public не может быть определена как будто она имеет уровень доступа private, потому что этот уровень доступа не может быть использован везде, где доступен public.
- Функция не может иметь уровень доступа выше чем у ее параметров или возвращаемого типа, потому что функция не может использоваться там, где ее параметры не доступны.
Более глубокий смысл данного руководящего принципа для различных аспектов раскрывается ниже.
Дефолтный уровень доступа
Все сущности вашего кода (кроме двух исключений, о которых мы поговорим чуточку позже) имеют дефолтный уровень доступа - внутренний (internal), если вы явно не указываете другой уровень. В результате во многих случаях вам не нужно указывать явный уровень доступа в вашем коде.
Уровень доступа для простых однозадачных приложений
Когда вы пишете простое однозадачное приложение, то код вашего приложения обычно самодостаточен и не требует доступа к нему из внешних источников. По умолчанию уровень доступа стоит внутренний, так что это полностью удовлетворяет требованию кода. Таким образом, вам не нужно указывать явно этот уровень доступа. Однако, если вам все таки нужно, то вы можете некоторые части вашего кода обозначить как fileprivate или private, для того чтобы спрятать детали реализации от другого кода этого же модуля.
Уровень доступа для фреймворка
Когда вы разрабатываете фреймворк, обозначьте внешний интерфейс фреймворка как open, public, так чтобы его можно было посмотреть и получить к нему доступ из других модулей, так например, чтобы приложение могло импортировать его. Внешний интерфейс - интерфейс прикладного программирования (API) для фреймворка.
Заметка
Любые внутренние детали реализации вашего фреймворка могут использоваться с дефолтным уровнем доступа internal, или они могут быть отмечены как fileprivate или private, если вы хотите их спрятать от остального внутреннего кода фреймворка. Вам нужно отметить объект как open или public, если вы хотите сделать его частью интерфейса фреймворка.
Уровни доступа для модуля поэлементного тестирования (unit test target)
Когда вы пишете приложение с модулем поэлементного тестирования, то код вашего приложения должен быть доступным для модуля, чтобы он мог его проверить. По умолчанию только сущности с маркировкой public могут быть доступны для других модулей, однако этот модуль может получить доступ ко всем внутренним сущностям, если вы поставили входную маркировку объявления модуля продукта как @testable и компилируете со включенным режимом тестирования.
Синтаксис контроля доступа
Определите уровень доступа для объекта, установив одно из ключевых слов (private, fileprivate, internal, public, open) перед вступительным словом сущности:
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() {}
Если вы не укажете уровень доступа, то он будет internal по умолчанию, о чем было поведано в главе Уровни доступа. Это значит, что SomeInternalClass и someInternalConstant могут быть записаны без явного указания модификатора уровня доступа, и они все равно будут все еще иметь internal уровень доступа:
class SomeInternalClass {} // неявно internal
let someInternalConstant = 0 // неявно internal
Пользовательские типы
Если вы хотите указать явно уровень доступа для пользовательского типа, то делайте это на этапе определения типа. Новый тип может быть использован там, где позволяет его уровень доступа. К примеру, если вы определите класс с уровнем доступа fileprivate, то он сможет быть использован только как тип свойства или параметр функции или возвращаемый тип в исходном файле, в котором определен этот fileprivate класс.
Контроль уровня доступа типа так же влияет на уровень доступа для этих членов по умолчанию (его свойств, методов, инициализаторов и сабскриптов). Если вы определяете уровень доступа типа как fileprivate или private, то дефолтный уровень доступа его членов так же будет fileprivate или private. Если вы определите уровень доступа как internal или public (или будете использовать дефолтный уровень доступа, без явного указания internal), то уровень доступа членов типа по умолчанию будет internal.
Заметка
Как уже было сказано выше, типы с уровнем доступа public по умолчанию имеют члены с уровнем доступа internal, а не public. Если вы хотите чтобы члены типа имели уровень доступа public, то вы должны явно указать его. Такое требование гарантирует, что внешняя часть API - эта та часть, которую вы выбираете сами и исключает тот случай, когда вы можете по ошибке забыть указать internal для внутреннего кода.
public class SomePublicClass { // явный public класс
public var somePublicProperty = 0 // явный public член класса
var someInternalProperty = 0 // неявный internal член класса
fileprivate func someFilePrivateMethod() {} // явный file-private член класса
private func somePrivateMethod() {} // явный private член класса
}
class SomeInternalClass { // неявный internal класс
var someInternalProperty = 0 // неявный internal член класса
fileprivate func someFilePrivateMethod() {} // явный file-private член класса
private func somePrivateMethod() {} // явный private член класса
}
fileprivate class SomeFilePrivateClass { // явный file-private класс
func someFilePrivateMethod() {} // неявный file-private член класса
private func somePrivateMethod() {} // явный private член класса
}
private class SomePrivateClass { // явный private класс
func somePrivateMethod() {} // неявный private член класса
}
Кортежи типов
Уровень доступа для кортежей типов имеет самый строгий уровень доступа типа из всех используемых типов в кортеже. Например, если вы скомпонуете кортеж из двух разных типов, один из которых будет иметь уровень доступа как internal, другой как private, то кортеж будет иметь уровень доступа как private.
Заметка
Кортежи типов не имеют отдельного определения в отличии от классов, структур, перечислений или функций. Уровень доступа кортежей типов вычисляется автоматически, когда используется кортеж, и не может быть указан явно.
Типы функций
Уровень доступа для типов функции вычисляется как самый строгий уровень доступа из типов параметров функции и типа возвращаемого значения. Вы должны указывать уровень доступа явно как часть определения функции, если вычисляемый уровень доступа функции не соответствует контекстному по умолчанию.
Пример ниже определяет глобальную функцию someFunction, без явного указания уровня доступа самой функции. Вы может быть ожидаете, что эта функция будет иметь уровень доступа по умолчанию internal, но только не в нашем случае. На самом деле функция, которая описана ниже вообще не будет компилироваться:
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// реализация функции…
}
Возвращаемый тип функцией является кортежем, который составлен из двух пользовательских классов, которые были определены ранее в этом разделе. Один из этих классов был определен как internal, другой - как private. Таким образом, общий уровень доступа кортежа будет вычислен как private (минимальный уровень доступа из всех элементов кортежа).
Из-за того, что уровень доступа функции private, то вы должны установить общий уровень доступа как private во время определения функции:
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// реализация функции…
}
Это неправильно ставить маркер уровня доступа функции someFunction как public или internal, или использовать уровень доступа по умолчанию, потому что пользователи функции с уровнем доступа public или internal не смогут получить соответствующий доступ к private классу, который используется в качестве части возвращаемого значения функции.
Типы перечислений
Каждый кейс в перечислении автоматически получает тот же уровень доступа, что и само перечисление. Вы не можете указать другой уровень доступа для какого-то определенного кейса перечисления.
В примере ниже перечисление CompassPoint имеет явный уровень доступа public. Случаи перечисления north, south, east и west так же получают такой же уровень доступа, то есть public:
public enum CompassPoint {
case north
case south
case east
case west
}
Исходные значения и связанные значения
Типы, используемые для любых начальных значений или связанных значений в перечислении, должны иметь как минимум такой же высокий уровень доступа как и перечисление. Вы не можете использовать тип private для типа исходного значения перечисления, которое имеет internal уровень доступа.
Вложенные типы
Вложенные типы, определенные внутри типа с уровнем доступа private, автоматически получают уровень доступа private. Вложенные типы внутри public типов или internal типов, автоматически получают уровень доступа как internal. Если вы хотите, чтобы вложенный тип внутри public типа имел уровень доступа как public, то вам нужно явно указать этот тип самостоятельно.
Уровень доступа класса и подкласса
Вы можете создать подкласс любого класса, который может быть доступен в текущем контексте. Подкласс не может иметь более высокого уровня доступа, чем его суперкласс. Например, вы не можете написать подклассу public, если его суперкласс имеет internal доступ.
В дополнение вы можете переопределить любой член класса (метод, свойство, инициализатор или сабскрипт), который будет виден в определенном контексте доступа.
Переопределение может сделать член унаследованного класса более доступным, чем его версия суперкласса. В примере ниже класс A имеет доступ public и имеет метод someMethod с уровнем доступа fileprivate. Класс B является подклассом класса A, который имеет урезанный уровень доступа до internal. Тем не менее, класс B предоставляет переопределение метода someMethod с уровнем доступа internal, который выше, чем первоначальное определение метода someMethod:
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {}
}
Член подкласса может вызвать член суперкласса, который имеет более низкий уровень доступа, чем член подкласса, до тех пор пока вызов члена суперкласса попадает под допустимый уровень доступа контекста (то есть, попадает в пределы одного и того же исходного файла, что и суперкласс - для вызова члена с уровнем доступа fileprivate или попадает в пределы одного и того же модуля, что и суперкласс - для вызова члена с уровнем доступа internal):
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {
super.someMethod()
}
}
Из-за того, что суперкласс A и подкласс B определены в одном исходном файле, то будет корректно для реализации B записать вызов метода someMethod как super.someMethod().
Константы, переменные, свойства и сабскрипт
Константы, переменные, свойства не могут быть более открытыми, чем их тип. Это не правильно писать свойство public для private типа. Аналогично дело обстоит и с сабскриптом: сабскрипт не может быть более открытым, чем тип индекса или возвращаемый тип.
Если константа, переменная, свойство или сабскрипт используют тип private, то они должны быть отмечены ключевым словом private:
private var privateInstance = SomePrivateClass()
Геттеры и сеттеры
Геттеры и сеттеры для констант, переменных и сабскриптов автоматически получают тот же уровень доступа как и константа, переменная, свойство или сабскрипт, которому они принадлежат.
Вы можете задать сеттер более низкого уровня доступа чем его соответствующий геттер, для ограничения области read-write этой переменной, свойства или сабскрипта. Вы присваиваете более низкий уровень доступа написав fileprivate(set), private(set) или internal(set) до вступительного var или subscript.
Заметка
Это правило применяется как к свойствам хранения так и к вычисляемым свойствам. Даже если вы не пишете явного геттера и сеттера для хранимого свойства, Swift все еще создает для вас неявный геттер и сеттер, чтобы вы могли получить доступ к хранимым свойствам. Используйте fileprivate(set), private(set) и internal(set) для изменения уровня доступа этого созданного сеттера в точно такой же форме как и в случае явного сеттера вычисляемого свойства.
Пример ниже определяет структуру TrackedString, которая отслеживает число изменений строкового свойства:
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
Структура TrackedString определяет свойство хранения value с начальным значением "" (пустая строка). Структура так же определяет свойство хранения numberOfEdits, которое используется для отслеживания количества изменений значения value. Эта модификация отслеживания реализована в наблюдателе didSet свойства value, которое увеличивает numberOfEdits каждый раз, как value получает новое значение.
Структура TrackedString и свойство value не указывают явного уровня доступа, таким образом они оба получают дефолтный уровень доступа internal. Однако уровень доступа numberOfEdits обозначен как private(set), что означает, что геттер свойства все еще имеет уровень доступа по умолчанию internal, но свойство может быть установлено только в пределах кода, который является частью структуры TrackedString. Это позволяет TrackedString изменять свойство numberOfEdits скрытно, но тем не менее позволяет свойству быть read-only (только для чтения), когда оно используется в других исходных файлах в пределах того же модуля.
Если вы создаете экземпляр 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("Количество изменений равно \(stringToEdit.numberOfEdits)")
// Выведет "Количество изменений равно 3"
Хотя вы и можете обращаться к текущему значению свойства numberOfEdits в пределах другого исходного файла, но вы не можете изменять его из другого исходного файла. Это ограничение защищает детали реализации функциональности TrackedString, в то же время обеспечивая удобный доступ к аспекту этой функциональности.
Обратите внимание, что вы можете присвоить явный уровень доступа и к геттеру, и к сеттеру, если это необходимо. Пример ниже показывает версию структуры TrackedString, где она определена с явным указанием открытого уровня доступа. Таким образом, элементы структуры (включая свойство numbersOfEdits) получают уровень доступа internal по умолчанию. Вы можете сделать уровень доступа геттера параметра 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, дефолтный инициализатор имеет уровень доступа internal. Если вы хотите, чтобы открытый (public) тип был инициализируемым при помощи инициализатора, который не имеет аргументов, когда используется в другом модуле, то вы должны явно указать такой инициализатор как часть определения типа.
Дефолтные почленные инициализаторы для типов структур
Дефолтные почленные инициализаторы для типов структур считаются частными (private), если есть свойства, которые имеют уровень доступа как private. В противном случае, инициализатор имеет уровень доступа internal.
Как и с дефолтным инициализатором выше, если вы хотите открытый тип структуры, который может быть инициализирован при помощи почленного инициализатора, когда используется в другом модуле, то вы должны предоставить открытый почленный инициализатор самостоятельно, как часть определения типа.
Протоколы и уровень доступа
Если вы хотите присвоить явный уровень доступа протоколу, то вы должны указать его во время определения протокола. Это позволяет вам создавать протоколы, которые могут быть приняты только внутри определенного уровня доступа контекста.
Уровень доступа каждого требования в процессе определения протокола устанавливается на тот же уровень, что и сам протокол. Вы не можете установить уровень доступа требований протокола отличным от того, который поддерживает сам протокол. Это гарантирует, что все требования протокола будут видимы любому типу, который принимает протокол.
Заметка
Если вы определяете public протокол, то требования протокола требуют public уровня доступа для тех требований, которые они реализуют. Это поведение отличается от поведений других типов, где определение открытого типа предполагает наличие уровня internal у элементов этого типа.
Наследование протокола
Если вы определяете новый протокол, который наследует из другого существующего протокола, то новый протокол может иметь уровень доступа не выше чем протокол, который он наследует. Вы не можете писать public протокол, который наследует из internal протокола, к примеру.
Соответствие протоколу
Тип может соответствовать протоколу с более низким уровнем доступа, чем сам тип. Например, вы можете определить public тип, который может быть использован в других модулях, но чье соответствие внутреннему протоколу может быть использовано только внутри модуля, где определен сам internal протокол.
Контекст, в котором тип соответствует конкретному протоколу, является минимумом из доступов протокола и типа. Если тип является public, но протокол, которому он соответствует является internal, то соответствие типа этому протоколу будет тоже internal.
Когда вы пишете или расширяете тип для того, чтобы он соответствовал протоколу, вы должны быть уверены, что реализация этого типа каждому требованию протокола, по крайней мере имеет один и тот же уровень доступа, что и соответствие типа этому протоколу. Например, если тип public соответствует протоколу internal, то реализация каждого требования протокола должна быть как минимум internal.
Заметка
В Swift как и в Objective-C соответствие протоколу является глобальным. И тип не может соответствовать протоколу двумя разными способами в пределах одной программы.
Расширения и уровни доступа
Вы можете расширить класс, структуру или перечисление в любом контексте, в котором класс, структура или перечисление доступны. Любой элемент типа, добавленный в расширение, имеет тот же дефолтный уровень доступа, что и типы, объявленные в исходном типе, будучи расширенными. Например, если вы расширяете тип public, то любые новые элементы этого типа, которые вы добавили, будут иметь уровень доступа равный internal. Если вы расширяете fileprivate тип, то дефолтный уровень доступа любого добавленного в него члена будет fileprivate. Если вы расширяете private тип, то дефолтный уровень доступа любого добавленного в него члена будет private.
Аналогично вы можете отметить расширение, явно указав модификатор уровня доступа (например, private extension), для того чтобы указать новый дефолтный уровень доступа, который будут иметь элементы, определенные в этом расширении. Этот новый уровень доступа может быть переопределен для отдельных элементов расширением.
Вы не можете предоставлять явный модификатор уровня доступа для расширения, если вы используете расширение для добавления соответствия протоколу. Вместо этого, собственный уровень доступа протокола используется для предоставления дефолтного уровня доступа для каждой реализации требования протокола внутри расширения.
Private свойства и методы в расширениях
Расширения, которые находятся в том же файле, что и сам класс/структура/перечисление, который(ую/ое) они расширяют, ведут себя точно так, как будто они являются частью расширяемого типа. И в результате вы можете:
- Объявлять приватные члены в оригинальном объявлении и получать доступ к ним через расширение
- Объявлять приватные члены в одном расширении и получать доступ к ним через другие расширения, если они находятся в том же файле
- Объявлять приватные члены в расширении и получать доступ к ним в оригинальном объявлении
Это поведение означает, что вы можете использовать расширения для организации вашего кода, независимо от того, имеют ли ваши типы приватные члены или нет. Например, нам дан вот такой простой протокол:
protocol SomeProtocol {
func doSomething()
}
Вы можете использовать расширение, чтобы реализовать требование протокола:
struct SomeStruct {
private var privateVariable = 12
}
extension SomeStruct: SomeProtocol {
func doSomething() {
print(privateVariable)
}
}
Универсальные шаблоны. Алиасы типов
Уровень доступа для универсального типа или универсальной функции вычисляется как минимальный уровень доступа универсального типа или самой функции и уровень доступа ограничений любого типа ограничений для параметров типа.
Алиасы типов
Любой алиас типа, который вы определяете, рассматривается как отдельный тип для цели контроля доступа. Алиас типа может иметь уровень доступа такой же или ниже, чем уровень доступа типа, псевдоним которого он создает. Например, алиас с уровнем доступа private, может быть алиасом для типа с уровнем доступа private, fileprivate, internal, public, open но если у алиаса уровень доступа стоит public, то он не может быть алиасом типа, у которого уровень доступа стоит как internal или private, fileprivate.
Заметка
Это правило так же применимо для алиасов типа связанных типов, используемых для удовлетворения несоответствий протоколу.