Знакомство со Swift

О языке Swift

Заметка

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

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

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

Swift исключает большой пласт распространенных программных ошибок при помощи применения современных программных паттернов:

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

Индексы массивов всегда проверяются на out-of-bounds ошибки.

Целые числа проверяются на переполнение.

Опционалы гарантируют, что значения nil будут явно обработаны.

Автоматическое управление памятью

Обработка ошибок позволяет осуществлять контролируемое восстановление от непредвиденных ошибок.

Код на Swift скомпилирован и оптимизирован, чтобы получать максимальную отдачу от современного оборудования. Синтаксис стандартной библиотеки спроектирован основываясь на руководстве, что самый очевидный и простой способ написания кода является лучшим вариантом. Комбинация безопасности и скорости делает Swift лучшим кандидатом для написания программ от уровня "Hello, World!" и до целой операционной системы.

Swift совмещает вывод типов и паттерн-матчинг с современным простым синтаксисом, позволяя сложным идеям быть выраженным просто и кратко. И в качестве результата не только становится проще писать код, но и читать его и поддерживать так же становится просто.

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

Совместимость версий Swift

Эта книга описывает Swift 4.2, дефолтную версию, которая по умолчанию поддерживается Xcode 10.0. Вы можете использовать Xcode 10.0 для сборки таргетов, которые написаны как на Swift 4, так и на Swift 3.

Заметка

Когда компилятор Swift 4.2 работает с кодом на Swift 3, он определяет версию языка как Swift 3.4. В качестве результата вы можете использовать условные завершающие блоки, например, #if swift(>=3.4), чтобы написать код, который совместим с несколькими версиями компилятора Swift.

Когда вы используете Xcode 9.2 для сборки кода на Swift 3, большая часть функционала Swift 4 так же будет вам доступна. Тем не менее есть функционал, который доступен только на Swift 4:

Операции с подстроками возвращают экземпляр типа Substring вместо String.

Атрибут @objc явно добавляется в нескольких местах.

Расширения типа в том же файле получают доступ и к приватным членам тоже.

Таргет написанный на Swift 4 может зависеть от таргета, который написан на Swift 3 и наоборот. Если у вас есть большой проект, который разделен на несколько фреймворков, то вы можете осуществить миграцию своего кода со Swift 3 на Swift 4 по одному фреймворку за раз.

Тур по Swift

По традиции, первая программа на новом языке должна выводить на экран словосочетание «Hello, world». На Swift это пишется всего в одну строку:

print("Hello, world!")

Если вы писали до этого код на C или Objective-C, то этот синтаксис должен быть вам знаком. На Swift, эта строка является законченной программой. Не нужно дополнительно импортировать отдельные библиотеки для таких функций, как ввод/вывод или обработка строк. Код, написанный в глобальной области, используется как входная точка для программы, так что функция main() больше не нужна. Также вам не нужно писать точки с запятой после каждой строки.

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

Задание

Для лучшего усвоения материала используйте Playground в Xcode. Playground позволяет видеть результат мгновенно при редактировании кода.

Скачать Playground

Простые значения

Используйте let для создания констант и var для объявления переменных. Значение константы не обязательно должно быть известно на момент компиляции, но оно должно присваиваться строго один раз. Это значит, что вы можете использовать константу для обозначения значения, определяемого единожды, но используемого во многих местах.

var myVariable = 42

myVariable = 50

let myConstant = 42

Константа или переменная должны иметь те же типы данных, которые вы хотите им присвоить. Хотя, вы не должны всегда явно объявлять тип. Когда вы присваиваете значение при создании константы или переменной, компилятор логически предугадывает его тип. В примере ниже, компилятор предугадал, что значение myVariable integer (целое число), потому что присвоенное ему значение - integer.

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

let implicitInteger = 70

let implicitDouble = 70.0

let explicitDouble: Double = 70

Задание

Создайте константу с явным типом Float (число с плавающей точкой) и значением 4.

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

let label = "The width is "

let width = 94

let widthLabel = label + String(width)

Задание

Попробуйте удалить конвертирование в String из последней строки. Какую ошибку вы получите?

Есть еще один простой способ поместить значение в строку. Запишите значение в скобках, и поставьте перед скобками обратный слэш (\), как показано ниже.

let apples = 3

let oranges = 5

let appleSummary = "I have \(apples) apples."

let fruitSummary = "I have \(apples + oranges) pieces of fruit."

Задание

Используйте \() для того, чтобы добавить выражение вычисления числа с плавающей точкой в строку, либо для того, чтобы вставить чье-то имя в выражение приветствия.

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

let quotation = """

Даже несмотря на то, что есть пробелы слева,

сама строка не имеет отступа.

За исключением этой строки.

Двойные кавычки (") могут быть внутри строки без экранирования.

У меня все еще есть \(apples + oranges) фруктов.

"""

Массивы и словари создаются с помощью квадратных скобок ([]), а получить доступ к их значениям можно указав индекс или ключ в квадратных скобках. Ставить запятую после последнего элемента разрешается.

var shoppingList = ["catfish", "water", "tulips", "blue paint"]

shoppingList[1] = "bottle of water"

var occupations = [

"Malcolm": "Captain",

"Kaylee": "Mechanic",

]

occupations["Jayne"] = "Public Relations"

Массив автоматически увеличивается при добавлении элементов.

shoppingList.append("blue paint")

print(shoppingList)

Чтобы создать пустой массив или словарь, используйте выражение инициализации.

let emptyArray = [String]()

let emptyDictionary = [String: Float]()

Если информация о типе переменной или константы должна быть предугадана, то вы можете написать пустой массив через [] и пустой словарь через [:] — например, когда вы присваиваете новое значение переменной или назначаете аргумент функции.

shoppingList = []

occupations = [:]

Управление потоком

Для создания условий используйте if и switch, а для создания циклов используйте for-in, while, и repeat-while. Скобки вокруг условий или циклов не обязательны. Фигурные скобки вокруг тела условия или цикла — обязательны.

let individualScores = [75, 43, 103, 87, 12]

var teamScore = 0

for score in individualScores {

if score > 50 {

teamScore += 3

} else {

teamScore += 1

}

}

print(teamScore)

В инструкции if, условие должно быть Boolean выражение — это означает, что такой код как if score {…} ошибочный, никакого неявного сравнения с нулем не будет.

Вы можете использовать if и let вместе, чтобы работать со значениями, которые могут отсутствовать. Эти значения представлены как опциональные. Опциональные значения либо содержат значение, либо содержат nil, который обозначает отсутствие значения. Можно писать вопросительный знак (?) после типа значения, чтобы обозначить что оно опциональное.

var optionalString: String? = "Hello"

print(optionalString == nil)

var optionalName: String? = "John Appleseed"

var greeting = "Hello!"

if let name = optionalName {

greeting = "Hello, \(name)"

}

Задание

Поменяйте optionalName на nil. Какое приветствие вы получите? Добавьте else условие, которое установит другое приветствие, если optionalName равно nil.

Если опциональное значение nil, то условие считается false и код в фигурных скобках пропускается. Иначе, опциональное значение извлекается и назначается константе написанной после let, что делает извлеченное значение доступным внутри блока кода.

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

let nickName: String? = nil

let fullName: String = "John Appleseed"

let informalGreeting = "Hi \(nickName ?? fullName)"

Switch поддерживает любые данные и множество операторов сравнения — они не ограничены целыми числами и сравнениями.

let vegetable = "red pepper"

switch vegetable {

case "celery":

print("Add some raisins and make ants on a log.")

case "cucumber", "watercress":

print("That would make a good tea sandwich.")

case let x where x.hasSuffix("pepper"):

print("Is it a spicy \(x)?")

default:

print("Everything tastes good in soup.")

}

Задание

Попробуйте удалить блок default. Какую ошибку вы получите?

Вы обратили внимание, как мы можем использовать let в примере, чтобы создать константу, значение которой соответствует условию?

После исполнения кода внутри switch блока case, который попал под условие, программа выходит из инструкции switch. Исполнение не продолжается к следующему блоку сase - это означает, что не нужно прерывать switch после каждого блока case с помощью оператора break.

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

let interestingNumbers = [

"Prime": [2, 3, 5, 7, 11, 13],

"Fibonacci": [1, 1, 2, 3, 5, 8],

"Square": [1, 4, 9, 16, 25],

]

var largest = 0

for (kind, numbers) in interestingNumbers {

for number in numbers {

if number > largest {

largest = number

}

}

}

print(largest)

Задание

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

Используйте while чтобы повторять код, пока условие не поменяется. Условие цикла может также быть в конце, если нужно чтобы цикл выполнился хотя-бы один раз.

var n = 2

while n < 100 {

n *= 2

}

print(n)

var m = 2

repeat {

m *= 2

} while m < 100

print(m)

Вы можете ставить индекс, используя оператор ..< чтобы создать диапазон индексов:

var firstForLoop = 0

for i in 0..<4 {

firstForLoop += i

}

print(firstForLoop)

Используйте ..< чтобы создать диапазон который будет пропускать последнее значение, либо используйте ... – диапазон который включает оба значения, и начальное и конечное.

Функции и замыкания

Чтобы объявить функцию используйте оператор func. Чтобы вызвать функцию просто напишите ее имя со списком аргументов в скобках. Используйте –> чтобы отделить имена и типы аргументов от возвращаемого типа функции.

func greet(name: String, day: String) -> String {

return "Hello \(name), today is \(day)."

}

greet(name: "Bob", day: "Tuesday")

Задание

Удалите аргумент day. Добавьте аргумент, чтобы вставить сегодняшнее блюдо дня в приветствие.

По умолчанию функции используют имена параметров как ярлыки для их аргументов. Напишите свой собственный ярлык до имени параметра функции, или напишите знак подчеркивания (_) для того, чтобы пропустить ярлык.

func greet(_ name: String, on day: String) -> String {

return "Hello \(name), today is \(day)."

}

greet("John", on: "Wednesday")

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

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {

var min = scores[0]

var max = scores[0]

var sum = 0

for score in scores {

if score > max {

max = score

} else if score < min {

min = score

}

sum += score

}

return (min, max, sum)

}

let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])

print(statistics.sum)

print(statistics.2)

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

func returnFifteen() -> Int {

var y = 10

func add() {

y += 5

}

add()

return y

}

returnFifteen()

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

func makeIncrementer() -> ((Int) -> Int) {

func addOne(number: Int) -> Int {

return 1 + number

}

return addOne

}

var increment = makeIncrementer()

increment(7)

Функция может принимать другую функцию в качестве аргумента.

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {

for item in list {

if condition(item) {

return true

}

}

return false

}

func lessThanTen(number: Int) -> Bool {

return number < 10

}

var numbers = [20, 19, 7, 12]

hasAnyMatches(list: numbers, condition: lessThanTen)

Функции на самом деле - частный случай замыканий. Замыкания представляют из себя блок кода, который может быть вызван позже. Код внутри замыканий имеет доступ к таким объектам, как переменные и функции, которые были созданы в тех же рамках, что и сами замыкания. Даже если замыкание находятся и запускается в другом блоке , вы уже видели этот пример во вложенных функциях. Вы можете написать замыкание без имени, просто обозначив код фигурными скобками и круглыми скобками ({}). Внутри скобок используйте in для разграничения аргументов и возвращаемого типа от тела замыкания.

numbers.map({ (number: Int) -> Int in

let result = 3 * number

return result

})

Задание

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

У вас есть несколько способов для того, чтобы написать замыкание более кратко. Когда тип замыкания точно известен, например - обратный вызов делегата (callback), вы можете пропустить тип его аргументов, тип возвращаемого значения, либо и то и другое. Одиночный оператор замыкания неявно возвращает значение своего единственного выражения.

let mappedNumbers = numbers.map({ number in 3 * number })

print(mappedNumbers)

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

let sortedNumbers = numbers.sorted { $0 > $1 }

print(sortedNumbers)

Объекты и классы

Чтобы создать класс используйте оператор class и дальше имя класса. Объявление свойств класса пишется таким же способом, как и объявление константы или переменной, за исключением того, что они объявляются в пределах класса. Подобно этому, методы класса объявляются тем же способом что и функции.

class Shape {

var numberOfSides = 0

func simpleDescription() -> String {

return "A shape with \(numberOfSides) sides."

}

}

Задание

Добавьте константное свойство класса, используя let. Также, добавьте другой метод, который принимает какой-нибудь параметр.

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

var shape = Shape()

shape.numberOfSides = 7

var shapeDescription = shape.simpleDescription()

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

class NamedShape {

var numberOfSides: Int = 0

var name: String

init(name: String) {

self.name = name

}

func simpleDescription() -> String {

return "A shape with \(numberOfSides) sides."

}

}

Обратите внимание, как self используется, чтобы различать name - как свойство класса, и name - как аргумент инициализатора. Аргументы инициализатору передаются как вызов функции при создании экземпляра класса. Каждому свойству должно присваиваться значение: либо через объявление (как с numberOfSides), либо через инициализатор (как с name).

Используйте deinit для создания деинициализатора, если вам нужно выполнить некоторую очистку, прежде чем объект будет освобожден.

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

Методы подкласса, которые переопределяют методы родителя, отмечаются с помощью оператора override. При попытке переопределения без override компилятор выдаст ошибку. Компилятор также обнаруживает методы с override, которые на самом деле не переопределяют никакие методы супер класса.

class Square: NamedShape {

var sideLength: Double

init(sideLength: Double, name: String) {

self.sideLength = sideLength

super.init(name: name)

numberOfSides = 4

}

func area() -> Double {

return sideLength * sideLength

}

override func simpleDescription() -> String {

return "A square with sides of length \(sideLength)."

}

}

let test = Square(sideLength: 5.2, name: "my test square")

test.area()

test.simpleDescription()

Задание

Создайте другой подкласс Circle класса NameShape, который принимает радиус и имя в качестве аргументов в их инициализатор. Реализуйте метод area() и метод simpleDescription() класса Circle.

В дополнение к простым хранящимся свойствам, свойства могут иметь также getter и setter.

class EquilateralTriangle: NamedShape {

var sideLength: Double = 0.0

init(sideLength: Double, name: String) {

self.sideLength = sideLength

super.init(name: name)

numberOfSides = 3

}

var perimeter: Double {

get {

return 3.0 * sideLength

}

set {

sideLength = newValue / 3.0

}

}

override func simpleDescription() -> String {

return "An equilateral triangle with sides of length \(sideLength)."

}

}

var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")

print(triangle.perimeter)

triangle.perimeter = 9.9

print(triangle.sideLength)

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

Обратите внимание, что инициализатор для EqualiateralTriangle имеет три разных шага:

Устанавливает значение свойств объявляемых подклассом.

Вызывает инициализатор родителя.

Изменяет значение свойств, объявленных родителем. Любые дополнительные работы по начальной установке, которые используют методы, getter’ы или setter’ы могут быть включены в этом месте.

Если вам не нужно вычислять свойство, но по прежнему нужно предоставить код, который будет запущен до и после установки нового значения - используйте willSet и didSet. Например, класс ниже проверяет что длина стороны треугольника всегда такая же, как длина стороны его квадрата:

class TriangleAndSquare {

var triangle: EquilateralTriangle {

willSet {

square.sideLength = newValue.sideLength

}

}

var square: Square {

willSet {

triangle.sideLength = newValue.sideLength

}

}

init(size: Double, name: String) {

square = Square(sideLength: size, name: name)

triangle = EquilateralTriangle(sideLength: size, name: name)

}

}

var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")

print(triangleAndSquare.square.sideLength)

print(triangleAndSquare.triangle.sideLength)

triangleAndSquare.square = Square(sideLength: 50, name: "larger square")

print(triangleAndSquare.triangle.sideLength)

Когда вы работаете с опциональными значениями, вы можете написать ? перед такими операциями как: методы, свойства или индексаторы. Если значение перед ? - nil, то все что после ? игнорируется и значение всего выражения становится nil. В противном случае, опциональное значение извлекается, и все что после ? выполняется. В обоих случаях значение всего выражения является опциональным значением.

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")

let sideLength = optionalSquare?.sideLength

Перечисления и структуры

Используйте слово enum для создания перечислений. Подобно классам и другим именованным типам, перечисления могут иметь методы связанные с ним.

enum Rank: Int {

case ace = 1

case two, three, four, five, six, seven, eight, nine, ten

case jack, queen, king

func simpleDescription() -> String {

switch self {

case .ace:

return "ace"

case .jack:

return "jack"

case .queen:

return "queen"

case .king:

return "king"

default:

return String(self.rawValue)

}

}

}

let ace = Rank.ace

let aceRawValue = ace.rawValue

Задание

Напишите функцию, сравнивающую два Rank значения, с помощью сравнения их исходных (raw) значений.

По умолчанию Swift присваивает исходные значения, начиная с нуля и увеличивая их на единицу каждый раз, но вы можете изменить это поведение, явно указав значения. В приведенном выше примере, Ace явно присвоено исходное значение 1, остальные исходные значения присваиваются по порядку. Вы также можете использовать строки или числа с плавающей точкой в качестве типа исходного значения перечисления. Используйте свойство rawValue для доступа к исходному значению элемента перечисления.

Используйте инициализатор init?(rawValue:) для того, чтобы создать экземпляр перечисления из исходного значения:

if let convertedRank = Rank(rawValue: 3) {

let threeDescription = convertedRank.simpleDescription()

}

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

enum Suit {

case spades, hearts, diamonds, clubs

func simpleDescription() -> String {

switch self {

case .spades:

return "spades"

case .hearts:

return "hearts"

case .diamonds:

return "diamonds"

case .clubs:

return "clubs"

}

}

}

let hearts = Suit.hearts

let heartsDescription = hearts.simpleDescription()

Задание

Добавьте метод color() для Suit, который возвращает "black" для spades (пики) и clubs (трефы), и возвращает "red" для hearts (червы) и diamonds (бубны).

Обратите внимание на два способа обращения к элементам перечисления Hearts выше. Во время присвоения значения константе hearts, обращение к элементу перечисления Suit.hearts происходит через его полное имя, потому что константа не имеет явно указанного типа. Внутри switch, обращение к перечислению происходит через сокращённую форму .hearts, потому что значение self уже известно, и оно Suit. Вы можете использовать сокращенную форму каждый раз, когда тип значения известен.

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

enum ServerResponse {

case result(String, String)

case failure(String)

}

let success = ServerResponse.result("6:00 am", "8:09 pm")

let failure = ServerResponse.failure("Out of cheese.")

switch success {

case let .result(sunrise, sunset):

print("Sunrise is at \(sunrise) and sunset is at \(sunset).")

case let .failure(message):

print("Failure... \(message)")

}

Задание

Добавьте третий кейс к ServerResponse и к инструкции switch.

Обратите внимание, как время восхода и время заката получаются из значения от ServerResponse в виде совпадающего случая в кейсе инструкции switch.

Используйте оператор struct для создания структур. Структуры поддерживают многие характерные черты классов, в том числе методы и инициализаторы. Одно из наиболее важных различий между классами и структурами в том, что структуры всегда копируются когда они передаются в коде, а классы, передаются по ссылке.

struct Card {

var rank: Rank

var suit: Suit

func simpleDescription() -> String {

return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"

}

}

let threeOfSpades = Card(rank: .three, suit: .spades)

let threeOfSpadesDescription = threeOfSpades.simpleDescription()

Задание

Добавьте метод для Card, который бы создал полную колоду карт, с одной картой из каждой комбинации ранга (rank) и масти (suit).

Протоколы и расширения

Используйте оператор protocol для объявления протокола.

protocol ExampleProtocol {

 var simpleDescription: String { get }

 mutating func adjust()

}

Классы, перечисления, и структуры могут соответствовать протоколам.

class SimpleClass: ExampleProtocol {

var simpleDescription: String = "A very simple class."

var anotherProperty: Int = 69105

func adjust() {

simpleDescription += " Now 100% adjusted."

}

}

var a = SimpleClass()

a.adjust()

let aDescription = a.simpleDescription

struct SimpleStructure: ExampleProtocol {

var simpleDescription: String = "A simple structure"

mutating func adjust() {

simpleDescription += " (adjusted)"

}

}

var b = SimpleStructure()

b.adjust()

let bDescription = b.simpleDescription

Задание

Добавьте дополнительное требование в протокол ExampleProtocol. Какие изменения Вам нужно внести в SimpleClass и SimpleStructure, чтобы они соответствовали требованиям этого протокола?

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

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

extension Int: ExampleProtocol {

var simpleDescription: String {

return "The number \(self)"

}

mutating func adjust() {

self += 42

}

}

print(7.simpleDescription)

Задание

Напишите расширение для типа Double, которое добавляет свойство absoluteValue.

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

let protocolValue: ExampleProtocol = a

print(protocolValue.simpleDescription)

// print(protocolValue.anotherProperty) // Раcкомментируйте, чтобы увидеть ошибку

Несмотря на то, что переменная protocolValue имеет исполняемый тип SimpleClass, компилятор обрабатывает его тип как присвоенный ему ExampleProtocol. Это означает, что вы не сможете случайно получить доступ к методам или свойствам, которые класс реализует в дополнение для соответствия протоколу.

Обработка ошибок

Вы отображаете ошибки, используя любой тип, который соответствует протоколу Error.

enum PrinterError: Error {

case outOfPaper

case noToner

case onFire

}

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

func send(job: Int, toPrinter printerName: String) throws -> String {

if printerName == "Never Has Toner" {

throw PrinterError.noToner

}

return "Job sent"

}

Есть несколько способов обработки ошибок. Один из вариантов использование do-catch блока. Внутри блока do, вы маркируете код, который может сгенерировать ошибку при помощи ключевого слова try перед ним. Внутри же блока catch автоматически присваивается имя error, но вы можете изменить его, указать свое собственное.

do {

let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")

print(printerResponse)

} catch {

print(error)

}

Задание

Поменяйте имя принтера на "Never Has Toner", так чтобы функция send(job:toPrinter:) сгенерировала ошибку.

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

do {

let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")

print(printerResponse)

} catch PrinterError.onFire {

print("I'll just put this over here, with the rest of the fire.")

} catch let printerError as PrinterError {

print("Printer error: \(printerError).")

} catch {

print(error)

}

Задание

Добавте код внутри блока do. Какого типа ошибка должна быть сгенерирована, чтобы она была обработана в первом catch блоке? А во втором? А в третьем?

Другим способом обработки ошибок является ключевое слово try?, которое позволяет изменить результат в опциональный тип. Если функция генерирует ошибку, то генерируется конкретная ошибка и результат становится равным nil. В противном случае, результат, содержащий опциональное значение возвращается функцией.

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")

let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

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

var fridgeIsOpen = false

let fridgeContent = ["milk", "eggs", "leftovers"]

func fridgeContains(_ food: String) -> Bool {

fridgeIsOpen = true

defer {

fridgeIsOpen = false

}

let result = fridgeContent.contains(food)

return result

}

fridgeContains("banana")

print(fridgeIsOpen)

Универсальные типы

Напишите имя внутри угловых скобок, чтобы создать универсальную (generic) функцию или тип.

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {

var result = [Item]()

for _ in 0..<numberOfTimes {

result.append(item)

}

return result

}

makeArray(repeating: "knock", numberOfTimes: 4)

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

// Reimplement the Swift standard library's optional type

enum OptionalValue<Wrapped> {

case none

case some(Wrapped)

}

var possibleInteger: OptionalValue<Int> = .none

possibleInteger = .some(100)

Используйте where после названия типа, чтобы указать список требований, например - потребовать, чтобы тип реализовал протокол, потребовать, чтобы два типа были одинаковы, или потребовать, чтобы класс имел определенный суперкласс.

func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool

where T.Element: Equatable, T.Element == U.Element {

for lhsItem in lhs {

for rhsItem in rhs {

if lhsItem == rhsItem {

return true

}

}

}

return false

}

anyCommonElements([1, 2, 3], [3])

Задание

Модифицируйте функцию anyCommonElements, чтобы сделать функцию, которая возвращает массив элементов, общих для обеих последовательностей.

Запись <T: Equatable> -это то же самое, что и <T> ... where T: Equatable.

Руководство Swift

Основы

Swift - новый язык программирования для разработки приложений под iOS, macOS, watchOS и tvOS. Несмотря на это, многие части Swift могут быть вам знакомы из вашего опыта разработки на C и Objective-C.

Swift предоставляет свои собственные версии фундаментальных типов C и Objective-C, включая Int для целых чисел, Double и Float для значений с плавающей точкой, Bool для булевых значений, String для текста. Swift также предоставляет мощные версии трех основных типов коллекций, Array, Set и Dictionary, как описано в разделе Типы коллекций.

Подобно C, Swift использует переменные для хранения и обращения к значениям по уникальному имени. Swift также широко использует переменные, значения которых не могут быть изменены. Они известны как константы, и являются гораздо более мощными, чем константы в C. Константы используются в Swift повсеместно, чтобы сделать код безопаснее и чище в случаях, когда вы работаете со значениями, которые не должны меняться.

В дополнение к знакомым типам, Swift включает расширенные типы, которых нет в Objective-C. К ним относятся кортежи, которые позволяют создавать и передавать группы значений. Кортежи могут возвращать несколько значений из функции как одно целое значение.

Swift также включает опциональные типы, которые позволяют работать с отсутствующими значениями. Опциональные значения говорят либо «здесь есть значение, и оно равно х», либо «здесь нет значения вообще». Опциональные типы подобны использованию nil с указателями в Objective-C, но они работают со всеми типами, не только с классами. Опциональные значения безопаснее и выразительнее чем nil указатели в Objective-C, и находятся в сердце многих наиболее мощных особенностей Swift.

Swift - язык типобезопасный, что означает, что Swift помогает вам понять, с какими типами значений ваш код может работать. Если кусок вашего кода ожидает String, безопасность типов не даст вам передать ему Int по ошибке. Кроме того, безопасность типов не позволит вам случайно передать опциональный String куску кода, который ожидает неопциональный String. Безопасность типов позволяет вам улавливать и исправлять ошибки как можно раньше в процессе разработки.

Константы и переменные

Константы и переменные связывают имя (например, maximumNumberOfLoginAttempts или welcomeMessage) со значением определенного типа (например, число 10 или строка "Hello"). Значение константы не может быть изменено после его установки, тогда как переменной может быть установлено другое значение в будущем.

Объявление констант и переменных

Константы и переменные должны быть объявлены, перед тем как их использовать. Константы объявляются с помощью ключевого слова let, а переменные с помощью var. Вот пример того, как константы и переменные могут быть использованы для отслеживания количества попыток входа, которые совершил пользователь:

let maximumNumberOfLoginAttempts = 10

var currentLoginAttempt = 0

Этот код можно прочесть как:

«Объяви новую константу с именем maximumNumberOfLoginAttempts, и задай ей значение 10. Потом, объяви новую переменную с именем currentLoginAttempt, и задай ей начальное значение 0.»

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

Вы можете объявить несколько констант или несколько переменных на одной строке, разделяя их запятыми:

var x = 0.0, y = 0.0, z = 0.0

Заметка

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

Аннотация типов

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

Этот пример добавляет обозначение типа для переменной с именем welcomeMessage, чтобы обозначить, что переменная может хранить String:

var welcomeMessage: String

Двоеточие в объявлении значит "...типа...", поэтому код выше может быть прочитан как:

«Объяви переменную с именем welcomeMessage, тип которой будет String»

Фраза «тип которой будет String» означает «может хранить любое String значение». Представьте, что словосочетание «тип которой будет такой-то» означает - значение, которое будет храниться.

Теперь переменной welcomeMessage можно присвоить любое текстовое значение, без каких либо ошибок:

welcomeMessage = "Hello"

Вы можете создать несколько переменных одного типа в одной строке, разделенной запятыми, с одной аннотацией типа после последнего имени переменной:

var red, green, blue: Double

Заметка

Редко когда вам понадобится обозначать тип на практике. Когда вы даете начальное значение константе или переменной на момент объявления, Swift всегда может вывести тип, который будет использовать в константе или переменной. Это описано в Строгая типизация и Вывод типов. В примере welcomeMessage выше, не было присвоения начального значения, так что тип переменной welcomeMessage указывается с помощью обозначения типа вместо того, чтобы вывести из начального значения.

Название констант и переменных

Вы можете использовать почти любые символы для названий констант и переменных, включая Unicode-символы:

let π = 3.14159

let 你好 = "你好世界"

let = "dogcow"

Имена констант и переменных не могут содержать пробелы, математические символы, стрелки, приватные (или невалидные) кодовые точки Unicode, а так же символы отрисовки линий или прямоугольников. Так же имена не могут начинаться с цифр, хотя цифры могут быть включены в имя в любом другом месте. Если вы объявили константу или переменную определенного типа, то вы не можете объявить ее заново с тем же именем или заставить хранить внутри себя значение другого типа. Также вы не можете изменить константу на переменную, а переменную на константу.

Заметка

Если вам нужно объявить константу или переменную тем же именем, что и зарезервированное слово Swift, то вы можете воспользоваться обратными кавычками (`) написанными вокруг этого слова. Однако старайтесь избегать имен совпадающих с ключевыми словами Swift и используйте такие имена только в тех случаях, когда у вас абсолютно нет выбора.

Вы можете изменить значение переменной на другое значение совместимого типа. В примере ниже значение friendlyWelcome изменено с “Hello!” на “Bonjour!”:

var friendlyWelcome = “Hello!”

friendlyWelcome = “Bonjour!”

// теперь friendlyWelcome имеет значение “Bonjour!”

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

let languageName = "Swift"

languageName = "Swift++"

// Это ошибка компилляции: languageName cannot be changed (значение languageName не может быть изменено).

Печать констант и переменных

Вы можете напечатать текущее значение константы или переменной при помощи функции print(_:separator:terminator:):

print(friendlyWelcome)

// Выведет "Bonjour!"

Функция print(_:separator:terminator:) является глобальной, которая выводит одно или более значений в подходящем виде. В Xcode, например, функция print(_:separator:terminator:) выводит значения в консоль. Параметры separator и terminator имеют дефолтные значения, так что при использовании функции их можно просто пропустить. По умолчанию функция заканчивает вывод символом переноса строки. Чтобы вывести в консоль значения без переноса на новую строку, вам нужно указать пустую строку в параметре terminator, например, print(_:separator:terminator:). Для получения дополнительной информации по дефолтным значениям параметров обратитесь в раздел "Значения по умолчанию для параметров".

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

print("Текущее значение friendlyWelcome равно \(friendlyWelcome)")

// Выведет "Текущее значение friendlyWelcome равно Bonjour!"

Заметка

Все опции, которые вы можете использовать в интерполяции строки вы сможете найти в разделе "Интерполяция строк".

Комментарии

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

Комментарии в Swift очень похожи на комментарии в C. Однострочные комментарии начинаются с двух слешей (//):

// это комментарий

Вы также можете написать многострочные комментарии, которые начинаются со слеша и звездочки (/*) и заканчиваются звездочкой, за которой следует слеш (*/):

/* это тоже комментарий,

но написанный на двух строках */

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

/* это начало первого многострочного комментария

/* это второго, вложенного многострочного комментария */

это конец первого многострочного комментария */

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

Точки с запятой

В отличие от многих других языков, Swift не требует писать точку с запятой (;) после каждого выражения в коде, хотя вы можете делать это, если хотите. Однако точки с запятой требуются, если вы хотите написать несколько отдельных выражений на одной строке:

let cat = "?"; print(cat)

// Выведет "?"

Целые числа

Integer (целое число) - это число, не содержащее дробной части, например как 42 и -23. Целые числа могут быть либо знаковыми (положительными, ноль или отрицательными) либо беззнаковыми (положительными или ноль).

Swift предусматривает знаковые и беззнаковые целые числа в 8, 16, 32 и 64 битном форматах. Эти целые числа придерживаются соглашения об именах, аналогичных именам в C, в том, что 8-разрядное беззнаковое целое число имеет тип Uint8, а 32-разрядное целое число имеет тип Int32. Как и все типы в Swift, эти типы целых чисел пишутся с заглавной буквой.

Границы целых чисел

Вы можете получить доступ к минимальному и максимальному значению каждого типа целого числа с помощью его свойств min и max:

let minValue = UInt8.min // minValue равен 0, а его тип UInt8

let maxValue = UInt8.max // maxValue равен 255, а его тип UInt8

Тип значения этих свойств соответствует размеру числа (в примере выше этот тип UInt8) и поэтому может быть использован в выражениях наряду с другими значениями того же типа.

Int

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

На 32-битной платформе, Int того же размера что и Int32

На 64-битной платформе, Int того же размера что и Int64

Если вам не нужно работать с конкретным размером целого числа, всегда используйте в своем коде Int для целых чисел. Это придает коду логичности и совместимости. Даже на 32-битных платформах, Int может хранить любое значение в пределах -2 147 483 648 и 2 147 483 647, а этого достаточно для многих диапазонов целых чисел.

UInt

Swift также предусматривает беззнаковый тип целого числа - UInt, который имеет тот же размер что и разрядность системы:

На 32-битной платформе, UInt того же размера что и UInt32

На 64-битной платформе, UInt того же размера что и UInt64

Заметка

Используйте UInt, только когда вам действительно нужен тип беззнакового целого с размером таким же, как разрядность системы. Если это не так, использовать Int предпочтительнее, даже когда известно, что значения будут неотрицательными. Постоянное использование Int для целых чисел способствует совместимости кода, позволяет избежать преобразования между разными типами чисел, и соответствует выводу типа целого числа, как описано в Строгая типизация и Вывод Типов.

Числа с плавающей точкой

Число с плавающей точкой - это число с дробной частью, например как 3.14159, 0.1, и -273.15.

Типы с плавающей точкой могут представлять гораздо более широкий спектр значений, чем типы целых значений, и могут хранить числа намного больше (или меньше) чем может хранить Int. Swift предоставляет два знаковых типа с плавающей точкой:

Double - представляет собой 64-битное число с плавающей точкой. Используйте его когда число с плавающей точкой должно быть очень большим или чрезвычайно точным

Float - представляет собой 32-битное число с плавающей точкой. Используйте его, когда значение не нуждается в 64-битной точности.

Заметка

Double имеет точность минимум 15 десятичных цифр, в то время как точность Float может быть всего лишь 6 десятичных цифр. Соответствующий тип числа с плавающей точкой используется в зависимости от характера и диапазона значений, c которыми вы должны работать в коде. В случаях, где возможно использование обоих типов, предпочтительным считается Double.

Строгая типизация и Вывод типов

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

Поскольку Swift имеет строгую типизацию, он выполняет проверку типов при компиляции кода и отмечает любые несоответствующие типы как ошибки. Это позволяет в процессе разработки ловить, и как можно раньше, исправлять ошибки.

Проверка типов поможет вам избежать ошибок при работе с различными типами значений. Тем не менее, это не означает, что при объявлении вы должны указывать тип каждой константы или переменной. Если вы не укажете нужный вам тип значения, Swift будет использовать вывод типов, чтобы вычислить соответствующий тип. Вывод типов позволяет компилятору вывести тип конкретного выражения автоматически во время компиляции, просто путем изучения значения, которого вы ему передаете.

Благодаря выводу типов, Swift требует гораздо меньше объявления типов, чем языки, такие как C или Objective-C. Константам и переменным все же нужно присваивать тип, но большая часть работы с указанием типов будет сделана за вас.

Вывод типов особенно полезен, когда вы объявляете константу или переменную с начальным значением. Часто это делается путем присвоения литерального значения (или литерала) к константам или переменным в момент объявления . (Литеральное значение - значение, которое появляется непосредственно в исходном коде, например как 42 и 3,14159 в примерах ниже.)

Например, если вы присваиваете литеральное значение 42 к новой константе не сказав какого она типа, Swift делает вывод, что вы хотите чтобы константа была Int, потому что вы присвоили ей значение, которое похоже на целое число:

let meaningOfLife = 42

// meaningOfLife выводится как тип Int

Точно так же, если вы не указали тип для литерала с плавающей точкой, Swift делает вывод, что вы хотите создать Double:

let pi = 3.14159

// pi выводится как тип Double

Swift всегда выбирает Double (вместо Float), когда выводит тип чисел с плавающей точкой.

Если объединить целые литералы и литералы с плавающей точкой в одном выражении, в этом случае тип будет выводиться как Double:

let anotherPi = 3 + 0.14159

// anotherPi тоже выводится как тип Double

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

Числовые литералы

Числовые литералы могут быть написаны как:

Десятичное число, без префикса

Двоичное число, с префиксом 0b

Восьмеричное число, с префиксом 0o

Шестнадцатеричное число, с префиксом 0x

Все эти литералы целого числа имеют десятичное значение 17:

let decimalInteger = 17

let binaryInteger = 0b10001 // 17 в двоичной нотации

let octalInteger = 0o21 // 17 в восмеричной нотации

let hexadecimalInteger = 0x11 // 17 в шестнадцатеричной нотации

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

Для десятичных чисел с показателем степени ехр, базовое число умножается на 10exp:

1.25e2 означает 1.25 × 102, или 125.0.

1.25e-2 означает 1.25 × 10-2, или 0.0125.

Для шестнадцатеричных чисел с показателем степени ехр, базовое число умножается на 2exp:

0xFp2 означает 15 × 22, или 60.0.

0xFp-2 означает 15 × 2-2, или 3.75.

Все эти числа с плавающей точкой имеют десятичное значение 12.1875:

let decimalDouble = 12.1875

let exponentDouble = 1.21875e1

let hexadecimalDouble = 0xC.3p0

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

let paddedDouble = 000123.456

let oneMillion = 1_000_000

let justOverOneMillion = 1_000_000.000_000_1

Преобразования числовых типов

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

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

Преобразования целых чисел

Диапазон значений, который может храниться в целочисленных константах и переменных, различен для каждого числового типа. Int8 константы и переменные могут хранить значения между -128 и 127, тогда как UInt8 константы и переменные могут хранить числа между 0 и 255. Если число не подходит для переменной или константы с определенным размером, выводится ошибка во время компиляции:

let cannotBeNegative: UInt8 = -1

// UInt8 не может хранить отрицательные значения, поэтому эта строка выведет ошибку

let tooBig: Int8 = Int8.max + 1

// Int8 не может хранить число больше своего максимального значения,

// так что это тоже выведет ошибку

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

Чтобы преобразовать один числовой тип в другой, необходимо создать новое число желаемого типа из существующего значения. Ниже, в примере, константа twoThousand имеет тип UInt16, тогда как константа one - UInt8. Сложить их напрямую не получится, потому что они разного типа. Вместо этого, в примере вызывается функция UInt16(one) для создания нового числа UInt16 из значения константы one:

let twoThousand: UInt16 = 2_000

let one: UInt8 = 1

let twoThousandAndOne = twoThousand + UInt16(one)

Теперь, из-за того, что обе части сложения имеют тип UInt16 - операция сложения допустима. Для конечной константы (twoThousandAndOne) выведен тип UInt16, потому что это сложение двух UInt16 значений.

НазваниеТипа(начальноеЗначение) - стандартный способ вызвать инициализатор типов Swift и передать начальное значение. Честно говоря, у UInt16 есть инициализатор, который принимает UInt8 значение, и, таким образом, этот инициализатор используется, чтобы создать новый UInt16 из существующего UInt8. Здесь вы не можете передать любой тип, однако это должен быть тип, для которого у UInt16 есть инициализатор. Расширение существующих типов с помощью создания инициализаторов, которые принимают новые типы (включая объявление вашего типа) рассматривается в главе Расширения.

Преобразования целых чисел и чисел с плавающей точкой

Преобразование между целыми числами и числами с плавающей точкой должно происходить явно:

let three = 3

let pointOneFourOneFiveNine = 0.14159

let pi = Double(three) + pointOneFourOneFiveNine

// pi равно 3.14159, и для него выведен тип Double

Здесь, значение константы three используется для создания нового значения типа Double, так что обе части сложения имеют один тип. Без этого преобразования сложение не будет проходить. Обратное преобразование числа с плавающей точкой в целое число тоже должно происходить явно. Так что тип целого числа может быть инициализирован с помощью Double и Float значений:

let integerPi = Int(pi)

// integerPi равен 3, и для него выведен тип Int

Числа с плавающей точкой всегда урезаются когда вы используете инициализацию целого числа через этот способ. Это означает, что 4.75 будет 4, а -3.9 будет -3.

Заметка

Правила объединения числовых констант и переменных отличается от правил числовых литералов. Литеральное значение 3 может напрямую сложиться с литеральным значением 0.14159, потому что числовые литералы сами по себе не имеют явного типа. Их значение выводится только когда исполняется компилятором.

Преобразования числовых типов

Псевдонимы типов задают альтернативное имя для существующего типа. Можно задать псевдоним типа с помощью ключевого слова typealias.

Псевдонимы типов полезны, когда вы хотите обратиться к существующему типу по имени, которое больше подходит по контексту, например, когда вы работаете с данными определенного размера из внешнего источника:

typealias AudioSample = UInt16

После того как вы один раз задали псевдоним типа, вы можете использовать псевдоним везде, где вы хотели бы его использовать

var maxAmplitudeFound = AudioSample.min

// maxAmplitudeFound теперь 0

Здесь AudioSample определен как псевдоним для UInt16. Поскольку это псевдоним, вызов AudioSample.min фактически вызовет UInt16.min, что показывает начальное значение 0 для переменной maxAmplitudeFound.

Логические типы

В Swift есть простой логический тип Bool. Этот тип называют логическим, потому что он может быть только true или false. Swift предусматривает две логические константы, true и false соответственно:

let orangesAreOrange = true

let turnipsAreDelicious = false

Типы для orangesAreOrange и turnipsAreDelicious были выведены как Bool, исходя из того факта, что мы им присвоили логические литералы. Так же как с Int и Double в предыдущих главах, вам не нужно указывать константы или переменные как Bool, если при создании вы присвоили им значения true или false. Вывод типов помогает сделать код Swift кратким и читабельным тогда, когда вы создаете константы или переменные со значениями которые точно известны.

Логические значения очень полезны когда вы работаете с условными операторами, такими как оператор if:

if turnipsAreDelicious {

 print("Mmm, tasty turnips!")

} else {

 print("Eww, turnips are horrible.")

}

// Выведет "Eww, turnips are horrible."

Условные операторы, такие как оператор if детально рассматриваются в главе Управление потоком.

Строгая типизация Swift препятствует замене значения Bool на не логическое значение. Следующий пример выведет ошибку компиляции:

let i = 1

if i {

 // этот пример не скомпилируется, и выдаст ошибку компиляции

}

Тем не менее, альтернативный пример ниже правильный:

let i = 1

if i == 1 {

 // этот пример выполнится успешно

}

Результат сравнения i == 1 имеет тип Bool, и поэтому этот второй пример совершает проверку типов. Такие сравнения как i == 1 обсуждаются в главе Базовые операторы.

Как в других примерах строгой типизации в Swift, этот подход предотвращает случайные ошибки и гарантирует, что замысел определенной части кода понятен.

Кортежи

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

В данном примере (404, "Not Found") это кортеж, который описывает код HTTP статуса. Код HTTP статуса — особое значение возвращаемое веб-сервером каждый раз, когда вы запрашиваете веб-страницу. Код статуса 404 Not Found возвращается, когда вы запрашиваете страницу, которая не существует.

let http404Error = (404, "Not Found")

// http404Error имеет тип (Int, String), и равен (404, "Not Found")

Чтобы передать код статуса, кортеж (404, "Not Found") группирует вместе два отдельных значения Int и String: число и понятное человеку описание. Это может быть описано как "кортеж типа (Int, String)".

Вы можете создать кортеж с любой расстановкой типов, и они могут содержать сколько угодно нужных вам типов. Ничто вам не мешает иметь кортеж типа (Int, Int, Int), или типа (String, Bool), или же с любой другой расстановкой типов по вашему желанию.

Вы можете разложить содержимое кортежа на отдельные константы и переменные, к которым можно получить доступ привычным способом:

let (statusCode, statusMessage) = http404Error

print("The status code is \(statusCode)")

// Выведет "The status code is 404"

print("The status message is \(statusMessage)")

// Выведет "The status message is Not Found"

Если вам нужны только некоторые из значений кортежа, вы можете игнорировать части кортежа во время разложения с помощью символа подчеркивания (_):

let (justTheStatusCode, _) = http404Error

print("The status code is \(justTheStatusCode)")

// Выведет "The status code is 404"

В качестве альтернативы можно получать доступ к отдельным частям кортежа, используя числовые индексы, начинающиеся с нуля:

print("The status code is \(http404Error.0)")

// Выведет "The status code is 404"

print("The status message is \(http404Error.1)")

// Выведет "The status message is Not Found"

Вы можете давать имена отдельным элементам кортежа во время объявления:

let http200Status = (statusCode: 200, description: "OK")

Когда вы присвоили имя элементу кортежа, вы можете обратиться к нему по имени:

print("The status code is \(http200Status.statusCode)")

// Выведет "The status code is 200"

print("The status message is \(http200Status.description)")

// Выведет "The status message is OK"

Кортежи особенно полезны в качестве возвращаемых значений функций. Функция, которая пытается получить веб-страницу, может вернуть кортеж типа (Int, String), чтобы описать успех или неудачу в поиске страницы. Возращая кортеж с двумя отдельными значениями разного типа, функция дает более полезную информацию о ее результате, чем, если бы, возвращала единственное значение одного типа , возвращаемое функцией. Для более подробной информации смотрите главу Функции, возвращающие несколько значений.

Заметка

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

Опциональные типы (опционалы)

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

Заметка

В C или Objective-C нет понятия опционалов. Ближайшее понятие в Objective-C это возможность вернуть nil из метода, который в противном случае вернул бы объект. В этом случае nil обозначает «отсутствие допустимого объекта». Тем не менее, это работает только для объектов, и не работает для структур, простых типов C, или значений перечисления. Для этих типов, методы Objective-C, как правило, возвращают специальное значение (например NSNotFound), чтобы указать отсутствие значения. Этот подход предполагает, что разработчик, который вызвал метод, знает, что есть это специальное значение и что его нужно учитывать. Опционалы Swift позволяют указать отсутствие значения для абсолютно любого типа, без необходимости использования специальных констант.

Приведем пример, который покажет, как опционалы могут справиться с отсутствием значения. У типа Int в Swift есть инициализатор, который пытается преобразовать значение String в значение типа Int. Тем не менее, не каждая строка может быть преобразована в целое число. Строка "123" может быть преобразована в числовое значение 123, но строка "hello, world" не имеет очевидного числового значения для преобразования.

В приведенном ниже примере используется метод Int() для попытки преобразовать String в Int:

let possibleNumber = "123"

let convertedNumber = Int(possibleNumber)

// для convertedNumber выведен тип "Int?", или "опциональный Int"

Поскольку метод Int() может иметь недопустимый аргумент, он возвращает опциональный Int, вместо Int. Опциональный Int записывается как Int?, а не Int. Знак вопроса означает, что содержащееся в ней значение является опциональным, что означает, что он может содержать некое Int значение, или он может вообще не содержать никакого значения. (Он не может содержать ничего другого, например Bool значение или значение String. Он либо Int, либо вообще ничто)

nil

Мы можем установить опциональную переменную в состояние отсутствия значения, путем присвоения ему специального значения nil

var serverResponseCode: Int? = 404

// serverResponseCode содержит реальное Int значение 404

serverResponseCode = nil

// serverResponseCode теперь не содержит значения

Заметка

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

Если объявить опциональную переменную без присвоения значения по умолчанию, то переменная автоматически установятся в nil для вас:

var surveyAnswer: String?

// surveyAnswer автоматически установится в nil

Заметка

nil в Swift не то же самое что nil в Objective-C. В Objective-C nil является указателем на несуществующий объект. В Swift nil не является указателем, а является отсутствием значения определенного типа. Устанавливаться в nil могут опционалы любого типа, а не только типы объектов.

Инструкция If и Принудительное извлечение

Вы можете использовать инструкцию if, сравнивая опционал с nil, чтобы проверить, содержит ли опционал значение. Это сравнение можно сделать с помощью оператора «равенства» (==) или оператора «неравенства» (!=).

Если опционал имеет значение, он будет рассматриваться как «неравным» nil:

if convertedNumber != nil {

  print("convertedNumber contains some integer value.")

}

// Выведет "convertedNumber contains some integer value."

Если вы уверены, что опционал содержит значение, вы можете получить доступ к его значению, добавив восклицательный знак (!) в конце имени опционала. Восклицательный знак фактически говорит: «Я знаю точно, что этот опционал содержит значение, пожалуйста, используй его». Это выражение известно как Принудительное извлечение значения опционала:

if convertedNumber != nil {

  print("convertedNumber has an integer value of \(convertedNumber!).")

}

// Выведет "convertedNumber has an integer value of 123."

Более подробную информацию об инструкции if можно получить в главе Управление потоком.

Заметка

Попытка использовать ! к несуществующему опциональному значению вызовет runtime ошибку. Всегда будьте уверены в том, что опционал содержит не-nil значение, перед тем как использовать ! чтобы принудительно извлечь это значение.

Привязка опционалов

Можно использовать Привязку опционалов, чтобы выяснить содержит ли опционал значение, и если да, то сделать это значение доступным в качестве временной константы или переменной. Привязка опционалов может использоваться с инструкциями if и while, для проверки значения внутри опционала, и извлечения этого значения в константу или переменную, в рамках одного действия. Инструкции if и while более подробно представлены в главе Управление потоком.

Привязку опционалов для инструкции if можно писать как показано ниже:

if let constantName = someOptional {

    statements

}

Мы можем переписать пример possibleNumber сверху, используя привязку опционалов, а не принудительное извлечение:

if let actualNumber = Int(possibleNumber) {

print("\(possibleNumber) has an integer value of \(actualNumber)")

} else {

print("\(possibleNumber) could not be converted to an integer")

}

// Выведет "123" has an integer value of 123

Это может быть прочитано как:

«Если опциональный Int возвращаемый Int(possibleNumber) содержит значение, установи в новую константу с названием actualNumber значение, содержащееся в опционале»

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

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

Вы можете включать столько опциональных привязок и логических условий в единственную инструкцию if, сколько вам требуется, разделяя их запятыми. Если какое-то значение в опциональной привязке равно nil, или любое логическое условие вычисляется как false, то все условие выражения будет считаться false. Следующие инструкции if эквиваленты:

if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {

print("\(firstNumber) < \(secondNumber) < 100")

}

// Выведет "4 < 42 < 100"

if let firstNumber = Int("4") {

if let secondNumber = Int("42") {

if firstNumber < secondNumber && secondNumber < 100 {

print("\(firstNumber) < \(secondNumber) < 100")

}

}

}

// Выведет "4 < 42 < 100"

Заметка

Константы и переменные, созданные через опциональную привязку в инструкции if, будут доступны только в теле инструкции if. В противоположность этому, константы и переменные, созданные через инструкцию guard, доступны в строках кода, следующих за инструкцией guard, что отражено в разделе Ранний Выход.

Неявно извлеченные опционалы

Как описано выше, опционалы показывают, что константам или переменным разрешено не иметь «никакого значения». Опционалы можно проверить с помощью инструкции if, чтобы увидеть существует ли значение, и при условии, если оно существует, можно извлечь его с помощью привязки опционалов для доступа к опциональному значению.

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

Эти виды опционалов называются неявно извлеченные опционалы. Их можно писать, используя восклицательный знак (String!), вместо вопросительного знака (String?), после типа, который вы хотите сделать опциональным.

Неявно извлеченные опционалы полезны, когда известно, что значение опционала существует непосредственно после первого объявления опционала, и точно будет существовать после этого. Неявно извлечённые опционалы в основном используются во время инициализации класса, как описано в разделе "Бесхозные ссылки и неявно извлеченные опциональные свойства".

Честно говоря, неявно извлеченные опционалы - это нормальные опционалы, но они могут быть использованы как не опциональные значения, без необходимости в извлечении опционального значения каждый раз при доступе. Следующий пример показывает разницу в поведении между опциональной строкой и неявно извлеченной опциональной строкой при доступе к их внутреннему значению как к явной строке:

let possibleString: String? = "An optional string."

let forcedString: String = possibleString! // необходим восклицательный знак

let assumedString: String! = "An implicitly unwrapped optional string."

let implicitString: String = assumedString // восклицательный знак не нужен

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

Заметка

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

Вы по прежнему можете обращаться к неявно извлеченному опционалу как к нормальному опционалу, чтобы проверить, содержит ли он значение:

if assumedString != nil {

print(assumedString)

}

// Выведет "An implicitly unwrapped optional string."

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

if let definiteString = assumedString {

print(definiteString)

}

// Выведет "An implicitly unwrapped optional string."

Заметка

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

Обработка ошибок

Вы используете обработку ошибок в ответ на появление условий возникновения ошибок во время выполнения программы.

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

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

func canThrowAnError() throws {

// эта функция может сгенерировать ошибку

}

Функция сообщает о возможности генерации ошибки, включив ключевое слово throw в сообщение. Когда вы вызываете функцию, которая может выбросить ошибку, вы добавляете ключевое слово try в выражение. Swift автоматически передает ошибки из их текущей области, пока они не будут обработаны условием catch.

do {

try canThrowAnError()

// ошибка не была сгенерирована

} catch {

// ошибка сгенерирована

}

Выражение do создает область, содержащую объект, которая позволяет ошибкам быть передаными в одно или несколько условий catch.

Вот пример того, как обработка ошибок может быть использована в ответ на различные условия возникновения ошибок:

func makeASandwich() throws {

// ...

}

do {

try makeASandwich()

eatASandwich()

} catch SandwichError.outOfCleanDishes {

washDishes()

} catch SandwichError.missingIngredients(let ingredients) {

buyGroceries(ingredients)

}

В этом примере, функция makeASandwich() генерирует ошибку, если нет чистых тарелок или если отсутствуют какие-либо ингредиенты. Так как makeASandwich() может выдавать ошибку, то вызов функции заворачивают в выражении try. При заворачивании вызова функции в выражение do, генерация каких-либо ошибок, будет передаваться на предусмотренные условия catch.

Если ошибка не генерируется, то вызывается функция eatASandwich(). Если ошибка все таки генерируется, и она соответствует SandwichError.outOfCleanDishes, то вызывается функция washDishes(). Если генерируется ошибка, и она соответствует SandwichError.missingIngredients , то функция buyGroceries(_:) вызывается с соответствующим значением [String], захваченным шаблоном catch.

Генерация, вылавливание и передача ошибок рассмотрены более подробно в главе Обработка ошибок.

Утверждения и предусловия

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

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

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

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

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

Отладка с помощью утверждений

Утверждения записываются как функция стандартной библиотеки Swift assert(_:_:file:line:). Вы передаете в эту функцию выражение, которые оценивается как true или false и сообщение, которое должно отображаться, если результат условия будет false. Например:

let age = -3

assert(age >= 0, "Возраст человека не может быть меньше нуля")

// это приведет к вызову утверждения, потому что age >= 0, а указанное значение < 0.

В этом примере, выполнение кода продолжится, только если age >= 0 вычислится в true, что может случиться, если значение age не отрицательное. Если значение age отрицательное, как в коде выше, тогда age >= 0 вычислится как false, и запустится утверждение. завершив за собой приложение.

Сообщение утверждения можно пропускать по желанию, как в следующем примере:

assert(age >= 0)

Если код уже проверяет условие, то вы используете функцию assertionFailure(_:file:line:) для индикации того, что утверждение не выполнилось. Например:

if age > 10 {

print("Ты можешь покататься на американских горках и чертовом колесе.")

} else if age > 0 {

print("Ты можешь покататься на чертовом колесе.")

} else {

assertionFailure("Возраст человека не может быть отрицательным.")

}

Обеспечение предусловиями

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

Вы пишете предусловие для вызова функции precondition(_:_:file:line:). Вы передаете этой функции выражение, которое вычисляется как true или false и сообщение, которое должно отобразиться, если условие будет иметь значение как false. Например:

// В реализации сабскрипта...

precondition(index > 0, "Индекс должен быть больше нуля.")

Вы так же можете вызвать функцию precondition(_:_:file:line:) для индикации, что отказ работы уже произошел, например, если сработал дефолтный кейс инструкции switch, когда известно, что все валидные значения должны быть обработаны любым кейсом, кроме дефолтного.

Заметка

Если вы компилируете в режиме -0unchecked, то предусловия не проверяются. Компилятор предполагает, что предусловия всегда получают значения true и он оптимизирует ваш код соответствующим образом. Однако, функция fatalError(_:file:line:) всегда останавливает исполнения, несмотря на настройки оптимизации.

Вы можете использовать функцию fatalError (_:file:line:) во время прототипирования или ранней разработки для создания заглушек для функциональности, которая еще не реализована, написав fatalError ("Unimplemented") в качестве реализации заглушки. Поскольку фатальные ошибки никогда не оптимизируются, в отличие от утверждений или предусловий, вы можете быть уверены, что выполнение кода всегда прекратится, если оно встречает реализацию заглушки.

Базовые операторы

Оператор — это специальный символ или выражение для проверки, изменения или сложения величин. Например, оператор сложения (+) суммирует два числа let i = 1 + 2, а логический оператор И && объединяет два логических значения if enteredDoorCode && passedRetinaScan.

Язык Swift поддерживает большинство стандартных операторов C, а также ряд возможностей для устранения типичных ошибок в коде. Оператор присваивания (=) не возвращает значение, что позволяет избежать путаницы с оператором проверки на равенство (==). Арифметические операторы (+, -, *, /, % и т. д.) могут обнаруживать и предотвращать переполнение типа, чтобы числовой переменной нельзя было присвоить слишком большое или слишком маленькое значение. Контроль переполнения типа включается в Swift специальными операторами, которые описаны в разделе Операторы переполнения.

В отличие от C язык Swift позволяет делить с остатком (%) числа с плавающей точкой. Также в Swift имеются два сокращенных оператора интервала (a..<b и a...b), которых нет в C.

В этой главе описываются стандартные операторы Swift. Более сложные операторы Swift рассмотрены в главе Продвинутые операторы, где описано, как объявить пользовательские операторы и реализовать стандартные операторы для пользовательских типов.

Терминология

Операторы делятся на унарные, бинарные и тернарные:

Унарные операторы применяются к одной величине (например, -a). Унарные префиксные операторы ставятся непосредственно перед величиной (например, !b), а унарные постфиксные операторы — сразу за ней (например, c!).

Бинарные операторы применяются к двум величинам (например, 2 + 3) и являются инфиксными, так как ставятся между этими величинами.

Тернарные операторы применяются к трем величинам. Как и в языке C, в Swift есть только один такой оператор, а именно — тернарный условный оператор (a ? b : c).

Величины, к которым применяются операторы, называются операндами. В выражении 1 + 2 символ + является бинарным оператором, а его операндами служат 1 и 2.

Оператор присваивания

Оператор присваивания (a = b) инициализирует или изменяет значение переменной a на значение b:

let b = 10

var a = 5

a = b

// теперь a равно 10

Если левая часть выражения является кортежем с несколькими значениями, его элементам можно присвоить сразу несколько констант или переменных:

let (x, y) = (1, 2)

// x равно 1, а y равно 2

В отличие от C и Objective-C оператор присваивания в Swift не может возвращать значение. К примеру, следующее выражение недопустимо:

if x = y {

 // это неверно, так как x = y не возвращает никакого значения

}

Эта особенность не позволяет разработчику спутать оператор присваивания (=) с оператором проверки на равенство (==). Благодаря тому, что выражения типа if x = y некорректны, подобные ошибки при программировании на Swift не произойдут.

Арифметические операторы

Язык Swift поддерживает четыре стандартных арифметических оператора для всех числовых типов:

сложение (+)

вычитание (-)

умножение (*)

деление (/)

1 + 2 // равно 3

5 - 3 // равно 2

2 * 3 // равно 6

10.0 / 2.5 // равно 4.0

В отличие от C и Objective-C арифметические операторы Swift по умолчанию не допускают переполнения типа. Контроль переполнения типа включается в Swift специальными операторами (например, a &+ b). Подробнее см. в главе Операторы переполнения.

Оператор сложения служит также для конкатенации, или же склейки, строковых значений (тип String):

"hello, " + "world" // равно "hello, world"

Оператор целочисленного деления

Оператор целочисленного деления (a % b) показывает, какое количество b помещается внутри a, и возвращает остаток деления a на b.

Заметка

Оператор целочисленного деления (%) в некоторых языках называется оператором деления по модулю. Однако учитывая его действие над отрицательными числами в Swift, этот оператор, строго говоря, выполняет деление с остатком, а не по модулю.

Оператор целочисленного деления работает следующим образом. Для вычисления выражения 9 % 4 сначала определяется, сколько четверок содержится в девятке:

В одной девятке содержатся две четверки, а остатком будет 1 (выделено оранжевым цветом).

На языке Swift это записывается так:

9 % 4 // равно 1

Чтобы получить результат деления a % b, оператор % вычисляет следующее выражение и возвращает остаток:

a = (b × множитель) + остаток

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

Подставляя в это выражение 9 и 4, получим:

9 = (4 × 2) + 1

Точно так же рассчитывается остаток, когда a отрицательно:

-9 % 4 // равно -1

Подставляя в наше выражение -9 и 4, получим:

-9 = (4 × -2) + -1

причем остаток будет равен -1.

Если b отрицательно, его знак отбрасывается. Это означает, что выражения a % b и a % -b всегда будут давать одинаковый результат.

Оператор унарного минуса

Для изменения знака числового значения служит префиксный минус -, который называется оператором унарного минуса:

let three = 3

let minusThree = -three // minusThree равно -3

let plusThree = -minusThree // plusThree равно 3, т. е. "минус минус три"

Оператор унарного минуса (-) ставится непосредственно перед значением, без пробела.

Оператор унарного плюса

Оператор унарного плюса (+) просто возвращает исходное значение без каких-либо изменений:

let minusSix = -6

let alsoMinusSix = +minusSix // alsoMinusSix равно -6

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

Cоставные операторы присваивания

Как и в языке C, в Swift имеются составные операторы присваивания, совмещающие простое присваивание (=) с другой операцией. Одним из примеров может служить оператор присваивания со сложением (+=):

var a = 1

a += 2

// теперь a равно 3

Выражение a += 2 является краткой формой записи a = a + 2. Таким образом, один и тот же оператор выполняет одновременно операцию сложения и присваивания.

Заметка

Составные операторы присваивания не возвращают значение. К примеру, нельзя написать так: let b = a += 2.

Для получения полного списка операторов присваивания, предусмотренных стандартной библиотекой Swift, см. Operator Declarations.

Операторы сравнения

Язык Swift поддерживает все стандартные операторы сравнения из C:

равно (a == b)

не равно (a != b)

больше (a > b)

меньше (a < b)

больше или равно (a >= b)

меньше или равно (a <= b)

Заметка

В языке Swift есть также два оператора проверки на идентичность/тождественность (=== и !==), определяющие, ссылаются ли два указателя на один и тот же экземпляр объекта. Дополнительную информацию см. в главе Классы и структуры.

Каждый оператор сравнения возвращает значение типа Bool, указывающее, является ли выражение истинным:

1 == 1 // истина, так как 1 равно 1

2 != 1 // истина, так как 2 не равно 1

2 > 1 // истина, так как 2 больше чем 1

1 < 2 // истина, так как 1 меньше 2

1 >= 1 // истина, так как 1 больше либо равно 1

2 <= 1 // ложь, так как 2 не меньше либо равно 1

Операторы сравнения часто используются в условных выражениях, включая инструкцию if:

let name = "world"

if name == "world" {

 print("hello, world")

} else {

 print("Мне жаль, \(name), но я тебя не узнаю")

}

// напечатает "hello, world", так как name очевидно равно "world"

Подробнее об инструкции if см. в главе "Управление потоком".

Вы так же можете сравнивать кортежи, которые имеют одно и то же количество значений, которые, в свою очередь, должны быть сравниваемыми, что означает, что кортеж типа (Int, String) может быть сравнен с кортежем такого же типа.

Кортежи сравниваются слева направо, по одному значению за раз до тех пор, пока операция сравнения не найдет отличия между значениями. Если все значения кортежей попарно равны, то и кортежи так же считаются равными. Например:

(1, "zebra") < (2, "apple") // true, потому что 1 меньше 2

(3, "apple") < (3, "bird") // true , потому что 3 равно 3, а "apple" меньше чем "bird"

(4, "dog") == (4, "dog") // true , потому что 4 равно 4 и "dog" равен "dog"

В примере выше, в первой строке вы можете видеть сравнение слева на право. Так как 1 меньше 2, то (1, “zebra”) меньше (2, “apple”), несмотря на остальные значения кортежа, потому что это неравенство было определено первыми членами. Не важно, что “zebra” не меньше, чем “apple”, потому что сравнение уже определено первыми элементами корней. Однако, когда первые элементы кортежей одинаковые, то сравниваются вторые элементы и так далее.

Кортежи могут сравниваться, только в том случае, если оператор сравнения можно применить ко всем членам кортежей соответственно. Например, как показано в коде ниже, вы можете сравнить два кортежа типа (String, Int) потому что и String, и Int могут сравниться оператором <. И наоборот, кортеж типа (String, Bool) сравниваться не может, так как к значениям типа Bool операторы сравнения не применяются.

("blue", -1) < ("purple", 1) // OK, расценивается как true

("blue", false) < ("purple", true) // Ошибка так как < не может применяться к значениям типа Bool

Заметка

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

Тернарный условный оператор

Тернарный условный оператор — это специальный оператор из трех частей, имеющий следующий синтаксис: выражение ? действие1 : действие2. Он выполняет одно из двух действий в зависимости от того, является ли выражение true или false. Если выражение равно true, оператор выполняет действие1 и возвращает его результат; в противном случае оператор выполняет действие2 и возвращает его результат.

Тернарный условный оператор является краткой записью следующего кода:

if выражение {

 действие1

} else {

 действие2

}

Ниже приведен пример расчета высоты строки в таблице. Если у строки есть заголовок, то она должна быть выше своего содержимого на 50 точек, а если заголовка нет, то на 20 точек:

let contentHeight = 40

let hasHeader = true

let rowHeight = contentHeight + (hasHeader ? 50 : 20)

// rowHeight равно 90

В развернутом виде этот код можно записать так:

let contentHeight = 40

let hasHeader = true

var rowHeight = contentHeight

if hasHeader {

    rowHeight = rowHeight + 50

} else {

    rowHeight = rowHeight + 20

}

// rowHeight равно 90

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

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

Оператор объединения по nil

Оператор объединения по nil (a ?? b) извлекает опционал a, если он содержит значение, или возвращает значение по умолчанию b, если a равно nil. Выражение a может быть только опционалом. Выражение b должно быть такого же типа, что и значение внутри a.

Оператор объединения по nil является краткой записью следующего кода:

a != nil ? a! : b

В вышеприведенном коде тернарный условный оператор и принудительное извлечение (a!) используются для обращения к значению внутри a, если a не равно nil, или для возвращения b в противном случае. Оператор объединения по nil — это более элегантный, короткий и понятный способ одновременно проверить условие и извлечь значение.

Заметка

Если a не равно nil, выражение b не анализируется. Такой подход называется краткой проверкой условия (short-circuit evaluation).

В следующем примере оператор объединения по nil выбирает между стандартным значением цвета и пользовательским:

let defaultColorName = "red"

var userDefinedColorName: String? // по умолчанию равно nil

var colorNameToUse = userDefinedColorName ?? defaultColorName

// userDefinedColorName равен nil, поэтому colorNameToUse получит значение по умолчанию — "red"

Переменная userDefinedColorName объявлена как строковый (String) опционал и по умолчанию равна nil. Так как userDefinedColorName является опционалом, ее значение можно анализировать посредством оператора объединения по nil. В вышеприведенном примере этот оператор задает начальное значение для строковой (String) переменной colorNameToUse. Так как userDefinedColorName равно nil, выражение userDefinedColorName ?? defaultColorName возвратит значение defaultColorName, т. е. "red".

Если переменной userDefinedColorName присвоить отличное от nil значение и снова передать ее в оператор объединения по nil, вместо значения по умолчанию будет использовано значение внутри userDefinedColorName:

userDefinedColorName = "green"

colorNameToUse = userDefinedColorName ?? defaultColorName

// userDefinedColorName не равно nil, поэтому colorNameToUse получит значение "green"

Операторы диапазона

В языке Swift есть два оператора диапазона, которые в короткой форме задают диапазон значений.

Оператор замкнутого диапазона

Оператор замкнутого диапазона (a...b) задает диапазон от a до b, включая сами a и b. При этом значение a не должно превышать b.

Оператор замкнутого диапазона удобно использовать при последовательном переборе значений из некоторого диапазона, как, например, в цикле for-in:

for index in 1...5 {

 print("\(index) умножить на 5 будет \(index * 5)")

}

// 1 умножить на 5 будет 5

// 2 умножить на 5 будет 10

// 3 умножить на 5 будет 15

// 4 умножить на 5 будет 20

// 5 умножить на 5 будет 25

Подробнее о циклах for-in см. в главе Управление потоком.

Оператор полузамкнутого диапазона

Оператор полузамкнутого диапазона (a..<b) задает диапазон от a до b, исключая значение b. Такой диапазон называется полузамкнутым, потому что он включает первое значение, но исключает последнее. Так же, как и для оператора замкнутого диапазона, значение a не должно превышать b. Если значение a равно значению b, то итоговый диапазон будет пустым.

Операторы полузамкнутого диапазона особенно удобны при работе с массивами и другими последовательностями, пронумерованными с нуля, когда нужно перебрать элементы от первого до последнего:

let names = ["Anna", "Alex", "Brian", "Jack"]

let count = names.count

for i in 0..<count {

 print("Person \(i + 1) будет \(names[i])")

}

// Person 1 будет Anna

// Person 2 будет Alex

// Person 3 будет Brian

// Person 4 будет Jack

Хотя в массиве четыре элемента, диапазон 0..<count доходит только до 3 (т. е. до номера последнего элемента в массиве), так как это оператор полузамкнутого диапазона. Подробнее о массивах см. в главе Массивы.

Односторонние диапазоны

Операторы замкнутого диапазона имеют себе альтернативу - диапазон, который продолжается насколько возможно, но только в одну сторону, например, диапазон, который включает все элементы массива, начиная от 2 и до последнего индекса. В этих случаях вы можете пропустить значение с одной стороны оператора диапазона. Этот тип диапазона называется односторонним, потому что оператор имеет значение только с одной стороны. Например:

for name in names[2...] {

print(name)

}

// Brian

// Jack

for name in names[...2] {

print(name)

}

// Anna

// Alex

// Brian

Оператор полузамкнутого диапазона так же имеет одностороннюю форму, которая записывается только с одним конечным значением. Точно так же как и в случае, когда вы включаете значение в обе стороны, конечное значение не является частью самого диапазона. Например:

for name in names[..<2] {

print(name)

}

// Anna

// Alex

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

let range = ...5

range.contains(7) // false

range.contains(4) // true

range.contains(-1) // true

Логические операторы

Логические операторы изменяют или комбинируют логические значения типа Boolean (булево) — true и false. Язык Swift, как и другие C-подобные языки, поддерживает три стандартных логических оператора:

логическое НЕ (!a)

логическое И (a && b)

логическое ИЛИ (a || b)

Оператор логического НЕ

Оператор логического НЕ (!a) инвертирует булево значение — true меняется на false, а false становится true.

Оператор логического НЕ является префиксным и ставится непосредственно перед значением, без пробела. Как видно из следующего примера, его можно воспринимать как "не a":

let allowedEntry = false

if !allowedEntry {

 print("ACCESS DENIED")

}

// Выведет "ACCESS DENIED"

Конструкция if !allowedEntry означает "если не allowedEntry". Идущая за ней строка будет выполнена, только если "не allowedEntry" является истиной, т. е. если allowedEntry равно false.

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

Оператор логического И

Оператор логического И (a && b) дает на выходе true тогда и только тогда, когда оба его операнда также равны true.

Если хотя бы один из них равен false, результатом всего выражения тоже будет false. На самом деле, если первое значение равно false, то второе даже не будет анализироваться, так как оно все равно не изменит общий результат на true. Такой подход называется краткой проверкой условия (short-circuit evaluation).

В следующем примере проверяются два значения типа Bool, и если они оба равны true, программа разрешает доступ:

let enteredDoorCode = true

let passedRetinaScan = false

if enteredDoorCode && passedRetinaScan {

 print("Welcome!")

} else {

 print("ACCESS DENIED")

}

// Выведет "ACCESS DENIED"

Оператор логического ИЛИ

Оператор логического ИЛИ (a || b) является инфиксным и записывается в виде двух вертикальных палочек без пробела. С его помощью можно создавать логические выражения, которые будут давать true, если хотя бы один из операндов равен true.

Как и описанный выше оператор логического И, оператор логического ИЛИ использует краткую проверку условия. Если левая часть выражения с логическим ИЛИ равна true, то правая не анализируется, так как ее значение не повлияет на общий результат.

В приведенном ниже примере первое значение типа Bool (hasDoorKey) равно false, а второе (knowsOverridePassword) равно true. Поскольку одно из значений равно true, результат всего выражения тоже становится true и доступ разрешается:

let hasDoorKey = false

let knowsOverridePassword = true

if hasDoorKey || knowsOverridePassword {

 print("Welcome!")

} else {

 print("ACCESS DENIED")

}

// Выведет "Welcome!"

Комбинирование логических операторов

Можно также составлять и более сложные выражения из нескольких логических операторов:

if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword {

 print("Welcome!")

} else {

 print("ACCESS DENIED")

}

// Выведет "Welcome!"

В этом примере с помощью нескольких операторов && и || составляется более длинное и сложное выражение. Однако операторы && и || по-прежнему применяются только к двум величинам, поэтому все выражение можно разбить на три простых условия. Алгоритм работы будет следующим:

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

Исходя из значений enteredDoorCode, passedRetinaScan и hasDoorKey первые два подусловия дают false. Однако был введен код экстренного доступа, поэтому все составное выражение по-прежнему равно true.

Заметка

Логические операторы Swift && и || являются лево-ассоциированными, что означает, что составные выражения с логическими операторами оценивают в первую очередь выражения слева направо.

Явное указание круглых скобок

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

if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {

 print("Welcome!")

} else {

 print("ACCESS DENIED")

}

// Выведет "Welcome!"

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

Строки и символы

Строка представляет собой совокупность символов, например "hello, world" или "albatross". Строки в Swift представлены типом String. К значению типа String можно получить доступ различными способами, включая и коллекцию значений типа Character.

Типы String и Character в Swift предусматривают быстрый, Unicode-совместимый способ работы с текстом в вашем коде. Синтаксис для создания и манипулирования строками легкий и читабельный, он включает синтаксис строковых литералов похожий на С. Конкатенация строк так же проста, как сложение двух строк с помощью оператора + , а изменчивостью строки можно управлять выбирая к чему присваивать значение, константе или переменной, также как в случае с любым другим значением в Swift. Вы также можете использовать строки для вставки констант, переменных, литералов и выражений в более длинные строки- процесс, известный как интерполяция строк. Это позволяет легко создавать пользовательские значения строк для отображения, хранения и вывода информации.

Несмотря на эту простоту синтаксиса, тип String в Swift имеет быструю и современную реализацию. Каждая строка состоит из независимых от кодировки символов Unicode, и обеспечивает поддержку доступа к этим символам в различных Unicode представлениях.

Заметка

Тип String в Swift бесшовно сшит с классом NSString из Foundation. Если вы работаете с фреймворком Foundation в Cocoa или Cocoa Touch, то весь API NSString доступен для каждого значения типа String создаваемого вами в Swift, включая все возможности String, которые описываются в этой главе. Вы также можете использовать значение с типом String для любых API, в которых используется NSString. Для получения дополнительной информации об использовании String с Foundation и Cocoa, обратитесь к книге "Использование Swift с Cocoa и Objective-C".

Строковые литералы

Вы можете включить предопределенные String значения в вашем коде как строковые литералы. Строковый литерал - это фиксированная последовательность текстовых символов, окруженная парой двойных кавычек ("").

Используйте строковый литерал как начальное значение для константы или переменной:

let​ ​someString​ = ​"Some string literal value"

Заметьте, что Swift вывел тип String для константы someString, потому что он был инициализирован строковым литеральным значением.

Многострочные литералы строк

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

let quotation = """

The White Rabbit put on his spectacles. "Where shall I begin,

please your Majesty?" he asked.

"Begin at the beginning," the King said gravely, "and go on

till you come to the end; then stop."

"""

Многострочный литерал строки включает в себя все строки между открывающими и закрывающими кавычками. Строка начинается на первой строке после открывающих кавычек ("""), а заканчивается на строке предшествующей закрывающим кавычкам, что означает, что ни одна из строк ниже ни начинается, ни заканчивается символом переноса строки:

let singleLineString = "These are the same."

let multilineString = """

These are the same.

"""

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

let softWrappedQuotation = """

The White Rabbit put on his spectacles. "Where shall I begin, \

please your Majesty?" he asked.

"Begin at the beginning," the King said gravely, "and go on \

till you come to the end; then stop."

"""

Для того, чтобы создать литерал строки, который начинается и заканчивается символом возврата каретки (\r), напишите пустую строчку в самом начале и в конце литерала строки, например:

let lineBreaks = """

This string starts with a line break.

It also ends with a line break.

"""

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

let linesWithIndentation = """

Эта строка начинается без пробелов в начале.

Эта строка имеет 4 пробела.

Эта строка так же начинается без пробелов.

"""

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

Специальные символы в строковых литералах

Строковые литералы могут включать в себя следующие специальные символы:

экранированные специальные символы \ 0 (нулевой символ), \\ (обратный слэш), \t (горизонтальная табуляция), \n (новая строка), \r (возвращение каретки), \ "(двойные кавычки) и \' (одиночные кавычки )

Произвольные скалярные величины Юникода, записанные в виде \u{n} , где n - 1-8 значное шестнадцатиричное число со значением, равным действительной точке кода Юникода.

Приведенный ниже код показывает все эти четыре примера специальных символов. wiseWords константа содержит два экранированных символа: двойные кавычки.  DollarSign, blackHeart, и sparklingHeart константы показывают скалярный формат Юникода:

let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"

// "Imagination is more important than knowledge" - Einstein

let dollarSign = "\u{24}" // $, Unicode scalar U+0024

let blackHeart = "\u{2665}" // ♥, Unicode scalar U+2665

let sparklingHeart = "\u{1F496}" // ♥, Unicode scalar U+1F496

Так как многострочные строки имеют три двойные кавычки вместо одной, то вы можете включить двойную кавычку внутрь многострочной строки без знака экранирования. Для того, чтобы включить символы """ в многострочную строку, вам нужно экранировать хотя бы одну из кавычек, например:

let threeDoubleQuotes = """

Escaping the first quote \"""

Escaping all three quotes \"\"\"

"""

Инициализация пустых строк

Чтобы создать пустое String значение в качестве отправной точки для создания более длинных строк, либо присвойте литерал пустой строки к переменной, либо инициализируйте объект String c помощью синтаксиса инициализации:

var emptyString = "" // empty string literal

var anotherEmptyString = String() // initializer syntax

// обе строки пусты и эквиваленты друг другу

Можно узнать пустое ли String значение, через его Boolean свойство isEmpty:

if emptyString.isEmpty {

 print("Nothing to see here")

}

// Выведет "Nothing to see here"

Изменчивость строк

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

var variableString = "Horse"

variableString += " and carriage"

// variableString теперь "Horse and carriage"

let constantString = "Highlander"

constantString += " and another Highlander"

// это выдаст ошибку компиляции: строковая константа не может быть модифицирована

Заметка

Этот подход отличается от изменчивости строк в Objective-C и Cocoa, где мы выбираем между двумя классами (NSString и NSMutableString), чтобы указать, может ли строка быть изменена.

Строка является типом значения

Тип String в Swift является типом значения. Когда вы создаёте новое String значение, это значение копируется когда оно передается функции или методу, или когда оно присваивается константе или переменной. В каждом случае создается новая копия существующего String значения, и передаётся либо присваивается новая копия, а не исходная версия. Типы значений описаны в главе Структуры и перечисления - типы значения.

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

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

Работа с символами

Тип String в Swift представляет собой коллекцию значений Character в указанном порядке. Вы можете получить доступ к отдельным значениям Character в строке с помощью итерации по этой строке в for-in цикле:

for character in "Dog! ?" {

print(character)

}

// D

// o

// g

// !

//

//?

Цикл for-in описан в главе Циклы for-in.

Кроме того, можно создать отдельную Character константу или переменную из односимвольного строкового литерала с помощью присвоения типа Character:

let exclamationMark: Character = "!"

Значения типа String могут быть созданы путем передачи массива типа [Character] в инициализатор:

let catCharacters: [Character] = ["C", "a", "t", "!", "?"]

let catString = String(catCharacters)

print(catString)

// Выведет "Cat!?"

Конкатенация строк и символов

Значения типа String могут быть добавлены или конкатенированы с помощью оператора сложения (+):

let string1 = "hello"

let string2 = " there"

var welcome = string1 + string2

// welcome равен "hello there"

Вы можете добавить значение типа String к другому, уже существующему значению String, с помощью комбинированного оператора сложения и присвоения (+=):

var instruction = "look over"

instruction += string2

// instruction равен "look over there"

Вы можете добавить значение типа Character к переменной типа String, используя метод String append:

let exclamationMark: Character = "!"

welcome.append(exclamationMark)

// welcome равен "hello there!"

Заметка

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

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

let badStart = """

one

two

"""

let end = """

three

"""

print(badStart + end)

// Prints two lines:

// one

// twothree

let goodStart = """

one

two

"""

print(goodStart + end)

// Prints three lines:

// one

// two

// three

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

Интерполяция строк

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

let multiplier = 3

let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"

// message равен "3 times 2.5 is 7.5"

В примере выше значение multiplier включено в строку как \(multiplier). В свою очередь \(multiplier) заменяется на фактическое значение константы multiplier, когда вычисляется интерполяция строки для создания конечного варианта.

Значение multiplier - это так же часть большего выражения в будущей строке. Это выражение высчитывает значение Double(multiplier) * 2.5 и вставляет результат 7.5 в строку. В этом случае выражение записанное в виде \(Double(multiplier) * 2.5) является строковым литералом.

Заметка

Выражения, которые вы пишите внутри скобок в интерполируемой строке, не должны содержать неэкранированный обратный слэш, символ перевода строки (\n) или символ возврат каретки (\r). Однако выражения могут содержать другие строковые литералы.

Юникод (Unicode)

Юникод является международным стандартом для кодирования, представления и обработки текста в разных системах письма. Он позволяет представлять почти любой символ из любого языка в стандартизированную форму. А также читать и записывать эти символы во внешний источник и из него. К таким источникам относятся, например текстовый файл или веб-страница. Типы String и Character в Swift полностью совместимы с Юникодом, что описано в этом разделе.

Скалярные величины Юникода

Если посмотреть глубже, то можно увидеть, что собственный String тип в Swift построен из скалярных значений (Unicode scalar ) Юникода. Скалярная величина Юникода является уникальным 21-разрядным числом для символа или модификатора, например, U+0061 для LATIN SMALL LETTER A ("a") , или U+1F425 для FRONT-FACING BABY CHICK ().

Заметка

Скалярная величина Юникода - это любая точка кода в диапазоне U+0000 до U+D7FF включительно, или U+E000 до U+10FFFF тоже включительно. Скалярные величины Юникода не включают Юникод суррогатные пары точек кода, т.е. точки кода в диапазоне U+D800 до U+DFFF включительно.

Обратите внимание, что не все 21-битные скалярные величины Юникода присваиваются символу. Некоторые скалярные величины содержатся в резерве для будущего присваивания. Скалярные величины, присвоенные символу, как правило имеют название, например LATIN SMALL LETTER A и FRONT-FACING BABY CHICK, что можно увидеть в примере выше.

Расширяемые наборы графем

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

Вот пример. Буква é может быть представлена в виде самостоятельной скалярной величины Юникода é (LATIN SMALL LETTER E WITH ACUTE, или U+00E9 ). Тем не менее, та же буква может быть представлена в виде пары скалярных величин-стандартной буквой е (LATIN SMALL LETTER E, или U+0065 ), и последующей скалярной величиной COMBINING ACUTE ACCENT (U + 0301). COMBINING ACUTE ACCENT графически присоединяется к предшествующей скалярной величине, превращая e в é , в момент, когда распознающая Юникод символы система, начинает рендер.

В обоих случаях буква é представляется в виде одного значения Character, которое представляет собой расширенный набор графем. В первом случае набор содержит одну скалярную величину; во втором случае две:

let eAcute: Character = "\u{E9}" // é

let combinedEAcute: Character = "\u{65}\u{301}" // e с последующим ́

// eAcute равен é, combinedEAcute равен é

Расширенный набор графем - это удобный способ представления многих сложных печатных символов, как одного значения Character. Например, Hangul звуки из корейского алфавита могут быть представлены либо как предварительно набранные, или наоборот, как разложенные ряды символов. Оба этих варианта определяются как одно Character значение в Swift:

let precomposed: Character = "\u{D55C}"

let decomposed: Character = "\u{1112}\u{1161}\u{11AB}"

//precomposed равен "한", decomposed равен "한"

Расширенный набор графем позволяет скалярам заключающих символов (например, COMBINING ENCLOSING CIRCLE, или U+20DD), заключать другие скаляры Юникода и выглядеть как значение типа Character.

let enclosedEAcute: Character = "\u{E9}\u{20DD}"

// enclosedEAcute равен é⃝

Скалярные величины Юникода для региональных символов могут быть объединены в пары для создания одного Character значения, таких как эта комбинация: REGIONAL INDICATOR SYMBOL LETTER U (U+1F1FA) и REGIONAL INDICATOR SYMBOL LETTER S (U+1F1F8):

let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"

// regionalIndicatorForUS равен

Подсчет символов

Чтобы получить количество значений Character в строке, используйте count для строки:

let unusualMenagerie = "Коала ?, Улитка ?, Пингвин ?, Верблюд ?"

print("unusualMenagerie содержит \(unusualMenagerie.count) символов")

// Выведет "unusualMenagerie содержит 39 символов"

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

Например, если вы инициализируете новую строку со словом из четырех символов cafe, а затем добавите COMBINING ACUTE ACCENT (U+0301) в ее конце, то результирующая строка будет по-прежнему иметь количество символов - 4 , с четвертым символ é , а не e:

var word = "cafe"

print("количество символов в слове \(word) равно \(word.count)")

// Выведет "количество символов в слове cafe равно 4"

word += "\u{301}" // COMBINING ACUTE ACCENT, U+0301

print("количество символов в слове \(word) равно \(word.count)")

// Выведет "количество символов в слове café равно 4"

Заметка

Расширенный набор графем может состоять из одного или более скалярных величин Юникода. Это означает, что различные символы, и различное отображение одного и того же символа, могут потребовать огромных объемов памяти для хранения. Из-за этого, символы в Swift не занимают одинаковый объем памяти в строке. В результате этого, количество символов в строке не может быть подсчитано без итерации в строке, для определения границ расширенного набора графем. Если вы работаете с особенно длинными значениями строк, имейте ввиду, что свойство count должно итерировать все скалярные величины Юникода в строке для того, чтобы определить символы в этой строке. Количество символов, возвращаемых значением count не всегда совпадает со свойством length у NSString, которое содержит те же символы. Длина NSString основывается на числе 16-битовых блоков кода в UTF-16 представлении строки, а не на количестве расширенных набора графем внутри строки.

Доступ и изменение строки

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

Индексы строки

Каждое String значение имеет связанный тип индекса: String.Index, что соответствует позиции каждого Character в строке.

Как было упомянуто выше, различные символы могут требовать различные объемы памяти для хранения, поэтому для того, чтобы определить, какой Character в определенной позиции, необходимо итерировать каждую скалярную величину Юникода, от начала или конца этой строки. По этой причине, Swift строки не могут быть проиндексированы целочисленными значениями.

Используйте свойство startIndex для доступа позиции первого Character в String. Свойство endIndex — это позиция после последнего символа в String. В результате, endIndex свойство не является допустимым значением для сабскрипта строки. Если String пустая, то startIndex и endIndex равны.

Вы получаете доступ к индексу до и после указанного индекса при помощи методов index(before:) и index(after:). Для доступа к индексу, расположенного не по соседству с указанным индексом, вы можете использовать метод index(_:offsetBy:) вместо многократного вызова предыдущих методов.

Вы можете использовать синтаксис индекса для доступа Character в определенном индексе String.

let greeting = "Guten Tag!"

greeting[greeting.startIndex]

// G

greeting[greeting.index(before: greeting.endIndex)]

// !

greeting[greeting.index(after: greeting.startIndex)]

// u

let index = greeting.index(greeting.startIndex, offsetBy: 7)

greeting[index]

// a

Попытка доступа к Character в индексе за пределами диапазона строки выдаст сообщение об ошибке выполнения.

greeting[greeting.endIndex] // ошибка

greeting.index(after: endIndex) // ошибка

Используйте свойство indices, чтобы создать Range всех индексов, используемых для доступа к отдельным символам строки.

for index in greeting.indices {

print("\(greeting[index]) ", terminator: " ")

}

// Выведет "G u t e n T a g !"

Заметка

Вы можете использовать свойства startIndex, endIndex и методы index(before:), index(after:) и index(_:offsetBy:) с любым типом, который соответствует протоколу Collection. Это включает в себя String, как и показано тут, различные типы коллекций, например Array, Dictionary и Set.

Добавление и удаление

Для того, чтобы вставить символ в строку по указанному индексу, используйте insert(_:at:) метод, а для того, чтобы вставить содержимое другой строки по указанному индексу, используйте метод insert(contentsOf:at:).

var welcome = "hello"

welcome.insert("!", at: welcome.endIndex)

// welcome теперь равен "hello!"

welcome.insert(contentsOf:" there", at: welcome.index(before: welcome.endIndex))

// welcome теперь равен "hello there!”

Для того, чтобы удалить символ из строки по указанному индексу используйте remove(at:), если вы хотите удалить значения по указанному диапазону индексов, используйте метод removeSubrange(_:):

welcome.remove(at: welcome.index(before: welcome.endIndex))

// welcome теперь равно "hello there"

let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex

welcome.removeSubrange(range)

// welcome теперь равно "hello”

Заметка

Вы можете использовать методы insert(_:at:), insert(contentsOf:at:), remove(at:) и removeSubrange(_:) с любыми типами, которые соответствуют протоколу RangeReplaceableCollection. Это включает в себя String, как показано тут, а так же коллекции, такие как Array, Dictionary и Set.

Подстроки

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

let greeting = "Hello, world!"

let index = greeting.index(of: ",") ?? greeting.endIndex

let beginning = greeting[..<index]

// beginning is "Hello"

// Конвертируем в строку для хранения более продолжительное время.

let newString = String(beginning)

Как и строки, каждая подстрока имеет область в памяти, где хранятся символы, создающие эту подстроку. Разница между строками и подстроками в том, что для оптимизации производительности подстрока может использовать часть памяти, используемую для хранения исходной строки или часть памяти, которая используется для хранения другой подстроки. (Строки так же имеют похожую оптимизацию, но если две строки делят между собой память, то они считаются равными.) Это оптимизация означает, что у вас не будет потери производительности через копирование памяти, если вы не изменяете строку или подстроку. Как уже было сказано ранее, подстроки не подходят для долгосрочного хранения, так как они повторно используют хранилище исходной строки. Исходная строка должна находиться в памяти до тех пор, пока одна из ее подстрок все еще используется.

В примере выше и greeting является строкой, которая имеет свою область памяти, где создающие ее символы хранятся. Так как и beginning, является подстрокой от greeting, то она переиспользует память. которую использует greeting. И наоборот, newString является строкой, которая была создана из подстроки и теперь она имеет свое хранилище. Рисунок ниже поможет нам разобраться с этими взаимоотношениями:

Заметка

И String и Substring реализуют протокол StringProtocol, что означает, что очень часто бывает удобно для строковых манипуляций принимать значение StringProtocol. Вы можете вызывать такие функции со значением String или Substring.

Сравнение строк

Swift предусматривает три способа сравнения текстовых значений: равенство строк и символов, равенство префиксов, и равенство суффиксов.

Равенство строк и символов

Равенство строк и символов проверяется оператором "равенства" (==) и оператором "неравенства" (!=), что описано в главе «Операторы сравнения»:

let quotation = "Мы с тобой похожи"

let sameQuotation = "Мы с тобой похожи"

if quotation == sameQuotation {

print("Эти строки считаются равными")

}

// Выведет "Эти строки считаются равными"

Два String значения (или два Character значения) считаются равными, если их расширенные наборы графем канонически эквивалентны . Расширенные наборы графем канонически эквивалентны, если они имеют один и тот же языковой смысл и внешний вид, даже если они изначально состоят из разных скалярных величин Юникода.

Например, LATIN SMALL LETTER E WITH ACUTE (U+00E9) канонически эквивалентна LATIN SMALL LETTER E(U+0065) , если за ней следует COMBINING ACUTE ACCENT (U+0301) . Оба этих расширенных набора графем являются допустимыми вариантами представления символа é , и поэтому они считаются канонически эквивалентными:

// "Voulez-vous un café?" используем LATIN SMALL LETTER E WITH ACUTE

let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"

// "Voulez-vous un café?" используем LATIN SMALL LETTER E и COMBINING ACUTE ACCENT

let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?"

if eAcuteQuestion == combinedEAcuteQuestion {

print("Эти строки считаются равными")

}

// Выведет "Эти строки считаются равными"

Наоборот, LATIN CAPITAL LETTER A (U+0041 или "A") , используемый в английском языке, не является эквивалентом CYRILLIC CAPITAL LETTER A (U+0410, или "А"), используемой в русском языке. Символы визуально похожи, но имеют разный языковой смысл:

let latinCapitalLetterA: Character = "\u{41}"

let cyrillicCapitalLetterA: Character = "\u{0410}"

if latinCapitalLetterA != cyrillicCapitalLetterA {

print("Эти строки считаются не равными")

}

// Выведет "Эти строки считаются не равными"

Заметка

Сравнение строк и символов в Swift не зависит от локализации.

Равенство префиксов и суффиксов

Чтобы проверить, имеет ли строка определенный строковый префикс или суффикс, вызовите hasPrefix(_:) и hasSuffix(_:) методы, оба из которых принимают единственный аргумент типа String, и возвращают логическое значение.

В приведенных ниже примерах рассмотрим массив строк, представляющих местоположение сцены в первых двух актах Ромео и Джульетты Шекспира:

let romeoAndJuliet = [

"Act 1 Scene 1: Verona, A public place",

"Act 1 Scene 2: Capulet's mansion",

"Act 1 Scene 3: A room in Capulet's mansion",

"Act 1 Scene 4: A street outside Capulet's mansion",

"Act 1 Scene 5: The Great Hall in Capulet's mansion",

"Act 2 Scene 1: Outside Capulet's mansion",

"Act 2 Scene 2: Capulet's orchard",

"Act 2 Scene 3: Outside Friar Lawrence's cell",

"Act 2 Scene 4: A street in Verona",

"Act 2 Scene 5: Capulet's mansion",

"Act 2 Scene 6: Friar Lawrence's cell"

]

Вы можете использовать hasPrefix(_:) метод с массивом romeoAndJuliet для подсчета количества сцен в первом акте пьесы:

var act1SceneCount = 0

for scene in romeoAndJuliet {

if scene.hasPrefix("Act 1 ") {

act1SceneCount += 1

}

}

print("Всего \(act1SceneCount) сцен в Акте 1")

// Выведет "Всего 5 сцен в Акте 1"

Точно так же, использование hasSuffix(_:) метода для подсчета количества сцен, которые происходят внутри или вокруг особняка Капулетти и клетки монаха Лоренцо:

var mansionCount = 0

var cellCount = 0

for scene in romeoAndJuliet {

if scene.hasSuffix("Capulet's mansion") {

mansionCount += 1

} else if scene.hasSuffix("Friar Lawrence's cell") {

cellCount += 1

}

}

print("\(mansionCount) сцен в особняке; \(cellCount) тюремные сцены")

// выводит "6 сцен в особняке; 2 тюремные сцены"

Заметка

hasPrefix(_:) и hasSuffix(_:) методы используются для символ-к-символу канонического эквивалентного сравнения между расширенными наборами графем в каждой строке, как описано в главе «Равенство строк и символов».

Юникод представления строк

Если строка Юникода записывается в текстовый файл или какое-либо другое хранилище, то скалярные величины Юникода в этой строке кодируются в одном из нескольких Юникод-определенных форм кодирования. Каждая форма кодирует строку мелкими кусками, известными как единица кода. Сюда включены: UTF-8 форма кодирования (которая кодирует строку в 8-битные блоки кода), UTF-16 форма кодирования (которая кодирует строку в качестве 16-битных блоков кода), и UTF-32 форма кодирования (которая кодирует строку в 32-битные единицы кода).

Swift предоставляет несколько разных способов доступа к отображению строк Юникода. Вы можете итерировать строки с for-in, для получения их индивидуальных Character значений, как расширенных наборов графем. Этот процесс описан в разделе "Работа с символами".

Кроме того, доступ к String значению в одном из трех других Юникод-совместимых отображений:

Набор UTF-8 единиц кода (можно получить доступ через свойство строки - utf8)

Набор UTF-16 единиц кода (можно получить доступ через свойство строки - utf16 )

Набор 21-битных скалярных значений Юникода, что эквивалентно форме кодирования UTF-32 (доступ через свойство строки - unicodeScalars)

Каждый пример, данный ниже, показывает разное отображение следующей строки, которая состоит из символов 'D', 'o', 'g', '!!' ("DOUBLE EXCLAMATION MARK", "U+203C") и , ? ("DOG FACE", "U+1F436") или скалярной величины Юникода U+203C):

let dogString = "Dog!!?"

Отображение UTF-8

Вы можете получить доступ к UTF-8 строке, итерируя его свойство utf8. Это свойство имеет тип String.UTF8View, который представляет собой набор значений unsigned 8-bit (UInt8), по одному для каждого байта в представлении UTF-8 строки:

for codeUnit in dogString.utf8 {

print("\(codeUnit) ", terminator: " ")

}

print("")

// 68 111 103 226 128 188 240 159 144 182

В приведенном выше примере, первые три десятичных codeUnit значения (68, 111, 103 ) отображают символы D, o, и g , чье UTF-8 отображение такое же, как и в ASCII отображении. Следующие три десятичных codeUnit значения (226, 128, 188 ) являются трех байтным UTF-8 отображением символа DOUBLE EXCLAMATION MARK. Последние четыре codeUnit значения (240, 159, 144, 182 ) являются четырех байтным UTF-8 отображением символа DOG FACE.

Отображение UTF-16

Вы можете получить доступ к UTF-16 строки, итерируя его свойство utf16. Это свойство имеет тип String.UTF16View, который представляет собой набор значений unsigned 16-bit (UInt16), по одному для каждого 16-разрядного блока кода в представлении UTF-16 строки:

for codeUnit in dogString.utf16 {

print("\(codeUnit) ", terminator: " ")

}

print("")

// Выведет "68 111 103 8252 55357 56374 "

И опять первые три десятичных codeUnit значения (68, 111, 103 ) отображают символы D, o, и g , чье UTF-16 отображение такое же, как и в строковом отображении UTF-8 (потому что эти скалярные величины Юникода отображают ASCII символы).

Четвертое codeUnit значение (8252) является десятичным эквивалентом шестнадцатеричного 203C, которое отображает скалярную величину Юникода U+203C для символа DOUBLE EXCLAMATION MARK. Этот символ может быть отображен в виде самостоятельной единицы кода в UTF-16.

Пятое и шестое codeUnit значение (55357 и 56374) являются суррогатной парой UTF-16 отображения символа DOG FACE. Эти значения имеют «высоко суррогатное» значение U+D83D (с десятичным значением 55357) и «низко суррогатное» значение U+DC36 (с десятичным значением 56374).

Отображение скалярных величин Юникода

Вы можете получить доступ к скалярному представлению Unicode значения String путем повторения его свойства unicodeScalars. Это свойство имеет тип UnicodeScalarView, который представляет собой коллекцию значений типа UnicodeScalar.

Каждый UnicodeScalar имеет свойство value, которое возвращает 21-битное значение скаляра, представленное в пределах UInt32:

for scalar in dogString.unicodeScalars {

print("\(scalar.value) ", terminator: " ")

}

print("")

// Выведет "68 111 103 8252 128054 "

Свойство value для первых трех десятичных codeUnit значений (68, 111, 103 ) снова отображают символы D, o, и g.

Четвертое codeUnit значение (8252) снова является десятичным эквивалентом шестнадцатеричного 203C, которое отображает скалярную величину Юникода U+203C для символа DOUBLE EXCLAMATION MARK.

Свойство value для пятого и последнего UnicodeScalar, 128054, является десятичным эквивалентом шестнадцатеричного значения 1F436, которое отображает скалярную величину Юникода U+1F436 для символа DOG FACE.

В качестве альтернативы обращения к свойствам value, каждое UnicodeScalar значение также может быть использовано для построения нового String значения, например, с помощью интерполяции строк:

for scalar in dogString.unicodeScalars {

print("\(scalar) ")

}

// D

// o

// g

// !!

// ?

Типы коллекций

Swift обеспечивает три основных типа коллекций - это Массивы, Множества и Словари для хранения коллекций значений. Массивы- это упорядоченные коллекции значений. Множества- это неупорядоченные коллекции уникальных значений. Словари- это неупорядоченные коллекции, хранящие пары "ключ-значение".

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

Заметка

Забегая вперед, скажем, что Массив, Словарь и Множество в Swift реализованы как универсальные коллекции. Более подробную информацию об универсальных типах и коллекциях можно получить в главе "Универсальные шаблоны".

Изменчивость коллекций

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

Заметка

Хорошей практикой является создание неизменяемых коллекций во всех случаях, когда коллекцию не нужно менять. Делая это, мы позволяем компилятору Swift оптимизировать производительность наших коллекций.

Массивы

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

Заметка

Массив в Swift связан с классом Foundation  NSArray. Для более подробной информации об использовании Array с Foundation и Cocoa, см. Bridging Between Array and NSArray.

Сокращённый синтаксис массивов

Полная форма записи массива в Swift пишется Array<Element>, где Element это тип значения, который может хранить массив.

Вы можете также написать массив в сокращенной форме как [Element].

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

Создание пустого массива

Вы можете создать пустой массив определенного типа с помощью синтаксиса инициализатора:

var someInts = [Int]()

print("someInts is of type [Int] with \(someInts.count) items.")

// Выведет "someInts is of type [Int] with 0 items."

Обратите внимание, что тип переменной someInts выводится как [Int] от типа инициализатора.

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

someInts.append(3)

// массив someInts теперь содержит одно значение типа Int

someInts = []

// массив someInts теперь пуст, но все равно имеет тип [Int]

Создание массива с дефолтным значением

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

var threeDoubles = Array(repeating: 0.0, count: 3)

// threeDoubles имеет тип [Double] и равен [0.0, 0.0, 0.0]

Создание массива, путем объединения двух массивов

Вы можете создать новый массив, объединив два существующих массива с совместимыми типами с оператором сложения (+). Новый тип массива выводится из типа двух массивов, которые вы объединяете вместе:

var anotherThreeDoubles = Array(repeating: 2.5, count: 3)

// anotherThreeDoubles имеет тип [Double] и равен [2.5, 2.5, 2.5]

var sixDoubles = threeDoubles + anotherThreeDoubles

// тип sixDoubles выведен как [Double] и равен [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]

Создание массива через литералы массива

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

Литерал массива пишется в виде списка значений, разделенных запятыми и окруженными парами скобок:

[значение 1, значение 2, значение 3]

В приведенном ниже примере создается массив под названием shoppingList для хранения String значений:

var shoppingList: [String] = ["Eggs", "Milk"]

// shoppingList был инициализирован с двумя элементами

Переменная shoppingList объявлена как "массив из String значений", который записывается как [String]. Поскольку для этого массива указан тип значения String, ему разрешено хранить только String значения. Здесь, массив shoppingList инициализирован двумя String значениями ("Eggs" и "Milk"), написанными внутри литерала массива.

Заметка

Массив shoppingList объявлен как переменная ( с помощью var ), а не константа ( с помощью let ), поскольку много элементов добавляются в список покупок в примерах ниже.

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

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

var shoppingList = ["Eggs", "Milk"]

Поскольку все значения внутри литерала массива одинакового типа, Swift может вывести, что [String] является правильным типом для переменной shoppingList.

Доступ и изменение массива

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

Чтобы узнать количество элементов в массиве, проверьте его read-only свойство count:

print("The shopping list contains \(shoppingList.count) items.")

// Выведет "The shopping list contains 2 items."

Логическое свойство isEmpty можно использовать в качестве быстрого способа узнать, является ли свойство count равным 0:

if shoppingList.isEmpty {

print("The shopping list is empty.")

} else {

print("The shopping list is not empty.")

}

// Выведет "The shopping list is not empty."

Вы можете добавить новый элемент в конец массива через вызов метода append:

shoppingList.append("Flour")

​// shoppingList теперь содержит 3 элемента, а кое-кто делает блины

Кроме того, добавить массив с одним или несколькими совместимыми элементами можно с помощью оператора сложения и присвоения (+=):

shoppingList += ["Baking Powder"]

​// shoppingList теперь хранит 4 элемента

shoppingList += ["Chocolate Spread", "Cheese", "Butter"]

​// shoppingList теперь хранит 7 элементов

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

var firstItem = shoppingList[0]

// firstItem равен "Eggs"

Заметка

Первый элемент в этом массиве имеет индекс 0, а не 1. Массивы в Swift всегда начинаются с 0.

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

shoppingList[0] = "Six eggs"

​// первый элемент в списке теперь равен "Six eggs", а не "Eggs"

Вы также можете использовать синтаксис сабскриптов для изменения диапазона значений за раз, даже если набор изменяющих значений имеет разную длину, по сравнению с диапазоном который требуется заменить. Следующий пример заменяет "Chocolate Spread", "Cheese", и "Butter" на "Bananas" и "Apples":

shoppingList[4...6] = ["Bananas", "Apples"]

​// shoppingList теперь содежит 6 элементов

Для вставки элемента по заданному индексу внутрь массива, вызовите его метод insert(_:at:) :

shoppingList.insert("Maple Syrup", at: 0)

​// shoppingList теперь содержит 7 элементов

​// "Maple Syrup" теперь первый элемент списка

Вызвав этот insert(_:at:) метод, мы вставили новый элемент со значением "Maple Syrup" в самое начало списка покупок, то есть в элемент с индексом 0.

Аналогичным образом можно удалить элемент из массива с помощью метода remove(at:). Этот метод удаляет элемент с указанным индексом и возвращает удалённый элемент (хотя вы можете игнорировать возвращаемое значение если оно вам не нужно):

let mapleSyrup = shoppingList.remove(at: 0)

​// элемент который имел индекс 0 был удален

// shoppingList теперь содержит 6 элементов, и нет Maple Syrup

​// константа mapleSyrup теперь равна удаленной строке "Maple Syrup"

Заметка

Если вы пытаетесь получить доступ или изменить значение индекса, который находится за пределами существующих границ массива, у вас будет ошибка исполнения. Вы можете проверить, действителен ли индекс, прежде чем его использовать, сравнив его с свойством count массива. За исключением случаев, когда count равен 0 (то есть массив пуст), самый большой валидный индекс в массиве всегда будет count - 1, так как массивы индексируются c нуля.

Любые пробелы внутри массива закрываются когда удаляется элемент, и поэтому значение с индексом 0 опять равно "Six eggs":

firstItem = shoppingList[0]

​// firstItem теперь равен "Six eggs"

Если вы хотите удалить последний элемент из массива, то можно использовать метод removeLast() вместо remove(at:), чтобы избежать необходимости запроса свойства count для массива. Также как и метод remove(at:), removeLast() возвращает удаленный элемент:

let​ ​apples​ = ​shoppingList​.​removeLast​()

// последний элемент массива был удален

// shoppingList теперь содержит 5 элементов, и нет яблок

// константа apples теперь равна удаленной строке "Apples"

Итерация по массиву

Вы можете выполнить итерацию по всему набору значений внутри массива с помощью цикла for-in :

for item in shoppingList {

print(item)

}

// Six eggs

// Milk

// Flour

// Baking Powder

// Bananas

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

for (index, value) in shoppingList.enumerated() {

print("Item \(index + 1): \(value)")

}

// Item 1: Six eggs

// Item 2: Milk

// Item 3: Flour

// Item 4: Baking Powder

// Item 5: Bananas

Чтобы получить подробную информацию про цикл for-in, смотрите главу Циклы For-in.

Множества

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

Заметка

Тип Swift Set связан с классом Foundation NSSet.

Получить больше информации об использовании множества с фреймворками Foundation и Cocoa можно в Bridging Between Set and NSSet.

Хеш значения для типа Set

Тип должен быть хешируемым для того, чтобы мог храниться в множестве, таким образом тип должен предоставлять возможность для вычисления собственного значения хеша. Тип значения хеша Int должен быть для всех объектов одинаковым, чтобы можно было провести сравнение что если a == b, то и a.hashValue == b.hashValue.

Все базовые типы Swift (Int, String, Double, Bool) являются хешируемыми типами по умолчанию и могут быть использованы в качестве типов значений множества или в качестве типов ключей словаря. Значения членов перечисления без каких-либо связанных значений (что описано в главе Перечисления) так же являются хешируемыми по умолчанию.

Заметка

Вы можете использовать ваш собственный тип в качестве типа значения множества или типа ключа словаря, подписав его под протокол Hashable из стандартной библиотеки Swift. Типы, которые подписаны под протокол Hashable должны обеспечивать gettable свойство hashValue. Значение, которое возвращает hashValue не обязательно должно иметь одно и то же значение при выполнении одной и той же программы или разных программ.

Так как протокол Hashable подписан под протокол Equatable, то подписанные под него типы так же должны предоставлять реализацию оператора равенства ==. Протокол Equatable требует любую реализацию оператора равенства для реализации возможности сравнения. Таким образом, реализация оператора == должна удовлетворять следующим трем условиям, для всех трех значений a, b, c.

a == a (Рефлексивность)

a == b, значит b == a (Симметрия)

b == a && b == c, значит a == c (Транзитивность)

Для более подробной информации читайте главу "Протоколы".

Синтаксис типа множества

Тип множества Swift записывается как Set<Element>, Element является типом, который хранится в множестве. В отличии от массивов множества не имеют сокращенной формы записи.

Создание и инициализация пустого множества

Вы можете создать пустое множество конкретного типа, используя синтаксис инициализатора:

var letters = Set<Character>()

print("letters имеет тип Set<Character> с \(letters.count) элементами.")

// Выведет "letters имеет тип Set<Character> с 0 элементов."

Заметка

Тип переменной letters выведен из типа инициализатора как Set<Character>.

Альтернативно, если контекст предоставляет информацию о типе, например как аргумент функции или просто явное указание типа переменной или константы, то вы можете создать пустое множество при помощи пустого литерала массива:

letters.insert("a")

// letters теперь содержит 1 элемент типа Character

letters = []

// letters теперь является пустым множеством, но все еще имеет тип Set<Character>

Создание множества при помощи литерала массива

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

Пример ниже создает множество favoriteGenres для хранения String.

var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]

// favoriteGenres был инициализирован при помощи трех начальных элементов

Переменная favoriteGenres объявлена как множество значений типа String, который записывается как Set<String>. Так как это множество имеет определенный тип String, то этому множеству позволено хранить только значения типа String. Поэтому здесь мы инициализируем favoriteGenres тремя значениями типа String, записанными в виде литерала массива.

Заметка

Множество favoriteGenres объявлен как переменная (ключевое слово var), но не константа (ключевое слово let), так как мы добавляем и удаляем элементы в примере ниже.

Так как тип множества не может быть выведен только из литерала, то его тип должен быть указан явно. Однако из-за вывода типа в Swift вы не должны писать тип множества, если вы инициализируете его при помощи литерала массива, который содержит элементы одного типа. Вместо этого инициализация favoriteGenres может быть записана и в более короткой форме:

var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]

Так как все элементы литерала массива одного типа, то Swift может вывести, что Set<String> является корректным типом для переменной favoriteGenres.

Доступ и изменение множества

Получить доступ и модифицировать множества можно через свойства и методы.

Для того, чтобы выяснить количество элементов в множестве вам нужно использовать свойство count:

print("У меня есть \(favoriteGenres.count) любимых музыкальных жанра.")

// Выведет "У меня есть 3 любимых музыкальных жанра."

Используйте булево свойство isEmpty в качестве сокращенной проверки наличия элементов во множестве или другими словами равно ли свойство count 0:

if favoriteGenres.isEmpty {

print("Мне все равно какая музыка играет. Я не придирчив.")

} else {

print("У меня есть свои музыкальные предпочтения.")

}

// Выведет "У меня есть свои музыкальные предпочтения."

Вы можете добавить новый элемент во множество, используя метод insert(_:):

favoriteGenres.insert("Jazz")

// теперь в favoriteGenres находится 4 элемента

Вы так же можете удалить элемент из множества, используя метод remove(_:), который удаляет элемент, который является членом множества и возвращает удаленное значение или nil, если удаляемого элемента нет. Так же все объекты множества могут быть удалены единовременно при помощи метода removeAll().

if let removedGenre = favoriteGenres.remove("Rock") {

print("\(removedGenre)? С меня хватит.")

} else {

print("Меня это не сильно заботит.")

}

// Выведет "Rock? С меня хватит."

Можно проверить наличие определенного элемента во множестве, используя метод contains(_:):

if favoriteGenres.contains("Funk") {

print("О! Да я встал с правильной ноги!")

} else {

print("Слишком много Funk'а тут.")

}

// Выведет "Слишком много Funk'а тут."

Итерация по множеству

Вы можете совершать итерации по множеству при помощи цикла for-in.

for genre in favoriteGenres {

print("\(genre)")

}

// Classical

// Jazz

// Hip hop

Для более подробной информации по циклу for-in читайте в главе Циклы For-in.

Множества в Swift не имеют определенного порядка. Для того, чтобы провести итерацию по множеству в определенном порядке, вам нужно использовать метод sorted(), который возвращает вам элементы коллекции в виде отсортированного массива, используя оператор <.

for genre in favoriteGenres.sorted() {

print("\(genre)")

}

// Classical

// Hip hop

// Jazz

Выполнение операций с множествами

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

Базовые операции множеств

Иллюстрации внизу изображают два множества a и b в результате применения различных методов.

Используйте метод intersection(_:) для создания нового множества из общих значений двух входных множеств.

Используйте метод symmetricDifference(_:) для создания нового множества из значений, которые не повторяются в двух входных множествах.

Используйте метод union (_:) для создания нового множества состоящего из всех значений обоих множеств.

Используйте метод subtracting (_:) для создания множества со значениями не принадлежащих указанному множеству из двух входных.

let oddDigits: Set = [1, 3, 5, 7, 9]

let evenDigits: Set = [0, 2, 4, 6, 8]

let singleDigitPrimeNumbers: Set = [2, 3, 5, 7]

oddDigits.union(evenDigits).sorted()

// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

oddDigits.intersection(evenDigits).sorted()

// []

oddDigits.subtracting(singleDigitPrimeNumbers).sorted()

// [1, 9]

oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()

// [1, 2, 9]

Взаимосвязь и равенство множеств

Иллюстрация ниже отображает три множества a, b и c. Множество a является надмножеством множества b, так как содержит все его элементы, соответственно множество b является подмножеством множества a, опять таки потому, что все его элементы находятся в a. Множества b и c называются разделенными, так как у них нет общих элементов.

Используйте оператор равенства (==) для определения все ли значения двух множеств одинаковы.

Используйте метод isSubset(of:) для определения все ли значения множества содержатся в указанном множестве.

Используйте метод isSuperset(of:), чтобы определить содержит ли множество все значения указанного множества.

Используйте методы isStrictSubset(of:) или isStrictSuperset(of:) для определения является ли множество подмножеством или надмножеством, но не равным указанному сету.

Используйте метод isDisjoint(with:) для определения того, отсутствуют ли общие значения в двух множествах или нет.

let houseAnimals: Set = ["", ""]

let farmAnimals: Set = ["", "", "", "", ""]

let cityAnimals: Set = ["", ""]

houseAnimals.isSubset(of: farmAnimals)

// true

farmAnimals.isSuperset(of: houseAnimals)

// true

farmAnimals.isDisjoint(with: cityAnimals)

// true

Словари

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

Заметка

Тип словаря в Swift имеет связь с классом Foundation NSDictionary. Более подробно об использовании словаря в  Foundation и Cocoa, см. Bridging Between Dictionary and NSDictionary.

Сокращенный синтаксис словаря

В Swift тип словаря в полной форме пишется как Dictionary<Key, Value>, где Key это тип значения который используется как ключ словаря, а Value это тип значения который словарь может хранить для этих ключей.

Заметка

Тип словаря Key должен подчиняться протоколу Hashable, как тип значения множества.

Вы можете также написать словарь в сокращенной форме как [Key: Value]. Хотя две формы функционально идентичны, краткая форма является предпочтительной и используется в данном руководстве при обращении к типу словаря.

Создание пустого словаря

Подобно массивам вы можете создать пустой словарь определенного типа с помощью синтаксиса инициализатора:

var namesOfIntegers = [Int: String]()

// namesOfIntegers является пустым [Int: String] словарем

В этом примере создается пустой словарь с типом [Int: String] для хранения удобных для восприятия имен числовых значений. Его ключи имеют тип Int, а значения - String.

Если контекст уже предоставляет информацию о типе, вы можете создать пустой словарь с помощью литерала пустого словаря, который пишется как [:] ( двоеточие внутри пары квадратных скобок):

namesOfIntegers[16] = "sixteen"

// namesOfIntegers теперь содержит 1 пару ключ-значение

namesOfIntegers = [:]

// namesOfIntegers теперь опять пустой словарь с типом [Int: String]

Создание словаря с литералом словаря

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

Пара ключ-значение является комбинацией ключа и значения. В литерале словаря, ключ и значение в каждой паре ключ-значение разделено двоеточием. Пары ключ-значение написаны как список, разделенный запятыми и окруженный парой квадратных скобок:

[ключ 1: значение 1, ключ 2: значение 2, ключ 3: значение 3]

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

var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

Словарь airports объявлен с типом [ String : String ], что означает "словарь ключи которого имеют тип String и значения которого также имеют тип String".

Заметка

Словарь airports объявлен как переменная ( с помощью var ), а не константа ( с помощью let ), поскольку много аэропортов будет добавляться к словарю в примерах ниже.

Словарь airports инициализирован с помощью литерала словаря, содержащего две пары ключ-значение. Первая пара имеет ключ "YYZ" и значение "Toronto Pearson". Вторая пара имеет ключ "DUB" и значение "Dublin".

Этот словарь содержит две пары String: String. Этот тип ключ-значение подходит типу который мы присвоили переменной airports ( словарь содержащий только String ключи, и только String значения), и поэтому присвоение литерала словаря допустимо в качестве способа инициализации словаря airports двумя начальным элементами.

Подобно массивам, вы можете не писать тип словаря если вы инициализируете его с помощью литерала словаря, чьи ключи и значения имеют соответствующие типы. Инициализация airports может быть записана в более краткой форме:

var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

Поскольку все ключи в литерале имеют одинаковый тип, и точно так же все значения имеют одинаковый тип, то Swift может вывести, что [String: String] является правильным типом для использования в словаре airports.

Доступ и изменение словаря

Вы можете получить доступ к словарю и изменять его либо через его методы и свойства, либо используя синтаксис индексов. Подобно массивам, вы можете узнать количество элементов в словаре через его read-only свойство count:

print("The airports dictionary contains \(airports.count) items.")

// Выведет "The airports dictionary contains 2 items."

Логическое свойство isEmpty можно использовать в качестве быстрого способа узнать, является ли свойство count равным 0:

if airports.isEmpty {

print("The airports dictionary is empty.")

} else {

print("The airports dictionary is not empty.")

}

// Выведет "The airports dictionary is not empty."

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

airports["LHR"] = "London"

// словарь airports теперь содержит 3 элемента

Вы также можете использовать синтаксис индексов для изменения значения связанного с определенным ключом:

airports["LHR"] = "London Heathrow"

// значение для "LHR" поменялось на "London Heathrow"

В качестве альтернативы индексам, можно использовать метод словаря updateValue(forKey:), чтобы установить или обновить значение для определенного ключа. Подобно примерам с индексами вверху, метод updateValue(forKey:) устанавливает значение для ключа если оно не существует, или обновляет значение, если этот ключ уже существует. Однако, в отличие от индексов, метод updateValue(forKey:) возвращает старое значение после выполнения обновления. Это позволяет вам проверить состоялось ли обновление или нет.

Метод updateValue(forKey:) возвращает опциональное значение соответствующее типу значения словаря. Например, для словаря, который хранит String значения, метод возвратит String? тип, или " опциональный String". Это опциональное значение содержит старое значение для этого ключа, если оно существовало до обновления, либо nil если значение не существовало.

if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {

print("The old value for DUB was \(oldValue).")

}

// Выведет "The old value for DUB was Dublin."

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

if let airportName = airports["DUB"] {

print("The name of the airport is \(airportName).")

} else {

print("That airport is not in the airports dictionary.")

}

// Выведет "The name of the airport is Dublin Airport."

Вы можете использовать синтаксис индексов для удаления пары ключ-значение из словаря путем присвоения nil значению для этого ключа:

airports["APL"] = "Apple International"

// "Apple International" несуществующий аэропорт для APL, так что удалим его

airports["APL"] = nil

// APL теперь был удален из словаря"

Кроме того, можно удалить пару ключ-значение из словаря с помощью метода removeValue(forKey:). Этот метод удаляет пару ключ-значение если она существует и затем возвращает значение , либо возвращает nil если значения не существует:

if let removedValue = airports.removeValue(forKey: "DUB") {

print("The removed airport's name is \(removedValue).")

} else {

print("The airports dictionary does not contain a value for DUB.")

}

// Выведет "The removed airport's name is Dublin Airport."

Итерация по словарю

Вы можете сделать итерацию по парам ключ-значение в словаре с помощью for-in цикла. Каждое значение в словаре возвращается как кортеж (ключ, значение),и вы можете разложить части кортежа по временным константам или переменным в рамках итерации:

for (airportCode, airportName) in airports {

print("\(airportCode): \(airportName)")

}

// LHR: London Heathrow

// YYZ: Toronto Pearson

Чтобы подробнее узнать про цикл for-in, смотрите главу Цикл for-in.

Вы также можете получить коллекцию ключей или значений словаря через обращение к его свойствам keys и values:

for airportCode in airports.keys {

print("Airport code: \(airportCode)")

}

// Airport code: LHR

// Airport code: YYZ

for airportName in airports.values {

print("Airport name: \(airportName)")

}

// Airport name: London Heathrow

// Airport name: Toronto Pearson

Если вам нужно использовать ключи или значения словаря вместе с каким-либо API, которое принимает объект Array, то можно инициализировать новый массив с помощью свойств keys и values:

let airportCodes = [String](airports.keys)

// airportCodes теперь ["YYZ", "LHR"]

let airportNames = [String](airports.values)

// airportNames теперь ["Toronto Pearson", "London Heathrow"]

Тип словаря в Swift является неупорядоченной коллекцией. Для итерации по ключам или значениям словаря в определенной последовательности, используйте метод sorted() для свойств keys или values словаря.

Управление потоком

В Swift есть все знакомые нам операторы управления потоком из C-подобных языков. К ним относятся: циклы for-in и while для многократного выполнения задач, инструкции if, guard и switch для выполнения различных ветвлений кода в зависимости от определенных условий, а также такие инструкции, как break и continue для перемещения потока выполнения в другую точку вашего кода.

Swift предоставляет цикл for-in, который упрощает итерацию по массивам, словарям, диапазонам, строкам и другим последовательностям.

В Swift инструкция switch также намного мощнее, чем его аналог из языка C. В Swift не происходит проваливания к следующему кейсу, что позволяет избежать распространенную в C ошибку, связанную с пропуском оператора break. Кейсы могут сопоставлять различные типы шаблонов, включая сопоставление диапазонов, кортежей, а также выполнять приведение к определенному типу. Совпавшие значения в кейсе оператора switch могут быть привязаны к временной константе или переменной для использования в теле кейса, а сложные условия сравнения могут быть выражены с помощью where для каждого кейса.

Циклы For-in

Цикл for-in используется для итерации по коллекциям элементов, таких как диапазоны чисел, элементы массива или символы в строке.

Можно использовать цикл for-in вместе с массивом для итерации по его элементам:

let names = ["Anna", "Alex", "Brian", "Jack"]

for name in names {

print("Hello, \(name)!")

}

// Hello, Anna!

// Hello, Alex!

// Hello, Brian!

// Hello, Jack!

Таким же образом вы можете производить итерацию по словарю, чтобы получить доступ к его паре ключ-значение. Когда происходит итерация по словарю, каждый его элемент возвращается как кортеж (ключ, значение). Вы можете разложить члены кортежа на отдельные константы для того, чтобы использовать их в теле цикла for-in. Здесь ключи словаря распадаются в константу animalName, а его значения - в константу legCount:

let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]

for (animalName, legCount) in numberOfLegs {

print("\(animalName)s have \(legCount) legs")

}

// ants have 6 legs

// cats have 4 legs

// spiders have 8 legs

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

Вы так же можете использовать for-in с числовыми диапазонами. Следующий пример напечатает несколько первых значений таблицы умножения на 5:

for index in 1...5 {

print("\(index) умножить на 5 будет \(index * 5)")

}

// 1 умножить на 5 будет 5

// 2 умножить на 5 будет 10

// 3 умножить на 5 будет 15

// 4 умножить на 5 будет 20

// 5 умножить на 5 будет 25

Коллекция элементов, по которой происходит итерация, является закрытым диапазоном чисел от 1 до 5 включительно, так как используется оператор закрытого диапазона(...). Значение index устанавливается в первое число из диапазона (1), и выражение внутри цикла выполняются. В данном случае, цикл содержит только одно выражение, которое печатает запись из таблицы умножения на пять для текущего значения index. После того как выражение выполнено, значение index обновляется до следующего значения диапазона (2), и функция print(_:separator:terminator:) снова вызывается. Этот процесс будет продолжаться до тех пор, пока не будет достигнут конец диапазона.

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

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

let base = 3

let power = 10

var answer = 1

for _ in 1...power {

answer *= base

}

print("\(base) to the power of \(power) is \(answer)")

// Выведет "3 в степени 10 равно 59049"

В этом примере вычисляется значение одного числа возведенное в степень другим (в данном случае 3 в степени 10). Начальное значение 1 (то есть 3 в степени 0) умножается на 3 десять раз, используя закрытый диапазон значений, который начинается с 1, и заканчивается 10. В данном случае нет необходимости знать значения счётчика во время каждой итерации цикла - он просто должен выполниться необходимое количество раз. Символ подчёркивания "_" (который используется вместо переменной цикла) игнорирует ее отдельные значения и не предоставляет доступ к текущему значению во время каждой итерации цикла.

В некоторых случаях вы можете не захотеть использовать замкнутый диапазон, который включает в себя оба конечных значения диапазона. Предположим, что вы хотите отрисовать минутные значения в виде черточек на часах. Вы будете рисовать 60 таких отметок, начиная с 0 минуты. Используйте полузамкнутый диапазон ( ..<), чтобы включить нижнюю границу, но не верхнюю. Для более подробного изучения диапазонов вам нужно перейти в главу Операторы диапазона.

let minutes = 60

for tickMark in 0..<minutes {

// render the tick mark each minute (60 times)

}

Некоторые пользователи, возможно, захотят иметь поменьше минутных делений, и, предположим, они захотят иметь отметки на циферблате только на каждые 5 минут. Для того, чтобы у нас была возможность пропустить ненужные временные отметки используйте функцию stride(from:to:by:).

let minuteInterval = 5

for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {

// render the tick mark every 5 minutes (0, 5, 10, 15 ... 45, 50, 55)

}

Так же вы можете работать и с закрытыми диапазонами, но уже при помощи метода stride(from:through:by:):

let hours = 12

let hourInterval = 3

for tickMark in stride(from: 3, through: hours, by: hourInterval) {

// render the tick mark every 3 hours (3, 6, 9, 12)

}

Циклы While

Цикл while выполняет набор инструкций до тех пор, пока его условие не станет false. Этот вид циклов лучше всего использовать в тех случаях, когда количество итераций до первого входа в цикл неизвестно. Swift предлагает два вида циклов while:

while - вычисляет условие выполнения в начале каждой итерации цикла.

repeat-while - вычисляет условие выполнения в конце каждой итерации цикла.

While

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

Общий вид цикла while выглядит следующим образом:

while условие {

инструкции

}

В этом примере показана простая игра Змеи и Лестницы (также известная, как Горы и Лестницы):

Игра проходит по следующим правилам:

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

Игрок начинает с "нулевого квадрата", который расположен в самом левом нижнем углу доски.

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

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

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

Игровая доска в примере представлена массивом значений типа Int. Его размер хранится в константе finalSquare, которая используется как для инициализации массива, так и для проверки условия победы. Игровое поле инициализируется 26-ю, а не 25-ю целочисленными нулевыми значениями(каждое с индексом от 0 до 25 включительно):

let finalSquare = 25

var board = [Int](repeating: 0, count: finalSquare + 1)

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

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02

board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

Квадрат 3 с основанием лестницы перемещает вас вверх на 11 квадрат. Чтобы это сделать, элементу массива board[03] присваивается +08, что эквивалентно значению 8 типа Int (разница между 3 и 11). Для того чтобы уточнить формулировку игрового поля, оператор унарного плюса (+i) уравновешивает оператор унарного минуса (-i), а числа ниже 10 приписаны нули. (В этих двух стилистических надстройках нет прямой необходимости, но они делают код более читаемым).

var square = 0

var diceRoll = 0

while square < finalSquare {

// бросок кубика

diceRoll += 1

if diceRoll == 7 { diceRoll = 1 }

// начать ходить на выпавшее количество шагов

square += diceRoll

if square < board.count {

// если мы все еще на поле, идти вверх или вниз по змеям или лестницам

square += board[square]

}

}

print("Game over!")

Данный пример использует самый простой подход к реализации броска кубика. Вместо использования генератора случайных чисел, значение diceRoll начинается с 0. Каждую итерацию цикла переменная diceRoll увеличивается на 1 с помощью инфиксного оператора (+= 1), после чего проверяется не стало ли её значение слишком большим. Возвращаемое значение += diceRoll равно значению переменной diceRoll после её инкрементирования. Когда это значение становится равным 7, оно сбрасывается на 1. В итоге мы получаем последовательность значений diceRoll, которая всегда будет выглядеть следующим образом: 1, 2, 3, 4, 5, 6, 1, 2 и так далее.

После броска кубика игрок перемещается вперед на количество клеток, равное значению переменной diceRoll. Возможен случай, когда бросок кубика может переместить игрока за пределы квадрата 25. В таком случае игра заканчивается. Для того чтобы справиться с таким сценарием, код проверяет что значение square меньше чем свойство count массива board перед прибавлением значения, хранящегося в board[square] к текущему значению square для перемещения игрока вверх или вниз по змеям или лестницам.

Заметка

Если бы этой проверки не было, могла бы произойти попытка обращения к значению board[square], находящемуся за границами массива board, что привело бы к вызову ошибки. Если square равно 26, код попытается проверить значение board[26], которое выходит за границы массива.

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

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

Цикл repeat-while

Другой вариант цикла while, известный как цикл repeat-while, выполняет одну итерацию до того, как происходит проверка условия. Затем цикл продолжает повторяться до тех пор, пока условие не станет false.

Заметка

Цикл repeat-while в Swift аналогичен циклу do-while в других языках.

Общий вид цикла repeat-while выглядит следующим образом:

repeat {

инструкции

} while условие

Ниже снова представлен пример игры Змеи и Лестницы , написанный с использованием цикла repeat-while. Значения переменных finalSquare, board, square и diceRoll инициализированы точно таким же образом, как и в случае с циклом while:

let finalSquare = 25

var board = [Int](repeating: 0, count: finalSquare + 1)

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02

board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

var square = 0

var diceRoll = 0

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

В начале игры игрок находится на квадрате 0. board[0] всегда равняется 0 и не оказывает никакого влияния:

repeat {

// идти вверх или вниз по змеям или лестницам

square += board[square]

// бросить кубик

diceRoll += 1

if diceRoll == 7 { diceRoll = 1 }

// начать ходить на выпавшее количество шагов

square += diceRoll

} while square < finalSquare

print("Game over!")

После проверки на наличие змей и лестниц происходит бросок кубика и игрок продвигается вперед на количество квадратов, равное diceRoll. После этого текущая итерация цикла заканчивается.

Условие цикла (while square < finalSquare) такое же, как раньше, но в этот раз оно не вычисляется до окончания первого запуска цикла. Структура цикла repeat-while лучше подходит для этой игры, чем цикл while в предыдущем примере. В цикле repeat-while выше square += board[square] всегда выполняется сразу, в то время как в цикле while происходит проверка того, что square все еще находится на поле. Такой принцип работы цикла repeat-while снимает необходимость проверки выхода за границы массива, которую мы видели в предыдущей версии игры.

Условные инструкции

Иногда бывает полезным исполнять различные куски кода в зависимости от условий. А может быть вы хотите запустить исполнение дополнительного кода, в случае возникновения ошибки или просто показать сообщение, когда значение какой-либо величины становится слишком большим. Чтобы сделать это, вы делаете ваш код условным.

Swift предоставляет нам два варианта добавить условные ответвления кода - это при помощи инструкции if и при помощи инструкции switch. Обычно мы используем инструкцию if, если наше условие достаточно простое и предусматривает всего несколько вариантов. А вот инструкция switch подходит для более сложных условий, с многими вариантами выбора, и очень полезна в ситуациях, где по найденному совпадению с условием и выбирается соответствующая ветка кода для исполнения.

Инструкция if

В самой простой своей форме инструкция if имеет всего одно условие if. Эта инструкция выполняет установленные инструкции только в случае, когда условие true:

var temperatureInFahrenheit = 30

if temperatureInFahrenheit <= 32 {

print ("It's very cold. Consider wearing a scarf.")

}

// Выведет "It's very cold. Consider wearing a scarf.

В приведенном примере проверяется значение температуры, которая может быть ниже или 32 (0 по Цельсию) градусов по Фаренгейту либо равна или выше. Если она ниже, то выведется сообщение. В противном случае никакого сообщения не будет, и код продолжит свое выполнение после закрывающей фигурной скобки инструкции if.

Инструкция if может предусматривать еще один дополнительный набор инструкций в ветке известной как оговорка else, которая нужна в случае, если условие false. Эти инструкции указываются через ключевое слово else:

temperatureInFahrenheit = 40

if temperatureInFahrenheit <= 32 {

print("It's very cold. Consider wearing a scarf.")

} else {

print("It's not that cold. Wear a t-shirt.")

}

// Выведет "It's not that cold. Wear a t-shirt."

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

Вы можете соединять инструкции if между собой, чтобы создать более сложные условия:

temperatureInFahrenheit = 90

if temperatureInFahrenheit <= 32 {

print("It's very cold. Consider wearing a scarf.")

} else if temperatureInFahrenheit >= 86 {

print("It's really warm. Don't forget to wear sunscreen.")

} else {

print("It's not that cold. Wear a t-shirt.")

}

// Выведет "It's really warm. Don't forget to wear sunscreen.

В приведенном коде была добавлена дополнительная инструкция if, для соответствия определенным температурам. Конечное условие else соответствует всем температурам, не соответствующим первым двум условиям.

Последняя else опциональна, однако она может быть удалена только в случае, если в ней нет необходимости:

temperatureInFahrenheit = 72

if temperatureInFahrenheit <= 32 {

print("It's very cold. Consider wearing a scarf.")

} else if temperatureInFahrenheit >= 86 {

print("It's really warm. Don't forget to wear sunscreen.")

}

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

Инструкция switch

Инструкция switch подразумевает наличие какого-то значения, которое сравнивается с несколькими возможными шаблонами. После того как значение совпало с каким-либо шаблоном, выполняется код, соответствующий ответвлению этого шаблона, и больше сравнений уже не происходит. Switch представляет собой альтернативу инструкции if, отвечающей нескольким потенциальным значениям.

В самой простой форме в инструкции switch значение сравнивается с одним или более значений того же типа:

switch значение для сопоставления {

case значение 1:

инструкция для значения 1

case значение 2, значение 3:

инструкция для значения 2 или значения 3

default:

инструкция, если совпадений с шаблонами не найдено

}

Каждая инструкция switch состоит из нескольких возможных случаев или cases, каждый из которых начинается с ключевого слова case. Помимо сравнения с конкретными значениями, Swift предлагает еще несколько опций для каждого случая для создания более сложных шаблонных сравнений. Об этих опциях мы поговорим далее в этой главе.

Тела каждого отдельного блока case в switch - это отдельная ветка исполнительного кода, что делает switch похожим на инструкцию if. Инструкция switch определяет какое ответвление должно быть выбрано. Это известно как переключение на значение, которое в настоящее время рассматривается.

Каждая инструкция switch должна быть исчерпывающей. То есть это значит, что каждое значение обязательно должно находить совпадение с шаблоном в каком-либо случае (case). Если неудобно вписывать все возможные варианты случаев, то вы можете определить случай по умолчанию, который включает в себя все значения, которые не были включены в остальные случаи. Такой случай по умолчанию называется default, и он всегда идет после всех остальных случаев.

В следующем примере switch рассматривает единичный символ в нижнем регистре, который называется someCharacter:

let someCharacter: Character = "z"

switch someCharacter {

case "a":

print("The first letter of the alphabet")

case "z":

print("The last letter of the alphabet")

default:

print("Some other character")

}

// Выведет "The last letter of the alphabet"

Первый кейс инструкции switch соответствует первой букве английского алфавита - a, второй кейс соответствует последней букве - z. Так как switch должна иметь кейс для каждого возможного символа, а не просто для каждой буквы алфавита, то в инструкции switch предусмотрен дефолтный кейс, который звучит как default, в который входят все символы кроме a и z. Как раз это условие гарантирует, что инструкция switch будет исчерпывающей.

Отсутствие case-провалов

Большое отличие инструкции switch в языке Swift от инструкции switch в C и Objective-C составляет отсутствие провалов через условия. Вместо этого инструкция switch прекращает выполнение после нахождения первого соответствия с case и выполнения соответствующего кода в ветке, без необходимости явного вызова break. Это делает инструкцию switch более безопасным и простым для использования, чем в C, и исключает исполнение кода более чем одного случая.

Заметка

Хотя break не требуются в Swift, вы все равно можете его использовать для соответствия и для игнорирования конкретного случая или просто для выхода из конкретного случая, еще до того, как исполнится код. Более детально можно прочитать в разделе Оператор Break в инструкции Switch.

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

let anotherCharacter: Character = "a"

switch anotherCharacter {

case "a": // ошибка, так как кейс имеет пустое тело

case "A":

print("The letter A")

default:

print("Not the letter A")

}

// ошибка компиляции

В отличии от инструкции switch в языке C, switch в Swift не соответствует ни "a", ни "A". Но зато вы получите ошибку компиляции о том, что case "a": не содержит ни одного исполняемого выражения. Такой подход исключает случайные "проваливания" из одного случая в другой, что делает код безопаснее и чище своей краткостью.

Для того, чтобы switch с одним кейсом подходил под "a" и "A",объедините два значения в один составной кейс, разделив значения запятыми.

let anotherCharacter: Character = "a"

switch anotherCharacter {

case "a", "A":

print("The letter A")

default:

print("Not the letter A")

}

// Выведет "The letter A"

Для того, чтобы составной кейс удобно было читать, он может быть написан несколькими строчками. Более подробно далее в "Составные Кейсы".

Заметка

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

Соответствие диапазону

Значения в кейсах switch могут быть проверены на их вхождение в диапазон. Пример ниже использует целочисленные диапазоны для описания любых значений художественным языком:

let approximateCount = 62

let countedThings = "moons orbiting Saturn"

var naturalCount: String

switch approximateCount {

case 0:

naturalCount = "no"

case 1..<5:

naturalCount = "a few"

case 5..<12:

naturalCount = "several"

case 12..<100:

naturalCount = "dozens of"

case 100..<1000:

naturalCount = "hundreds of"

default:

naturalCount = "many"

}

print("There are \(naturalCount) \(countedThings).")

// Выводит "There are dozens of moons orbiting Saturn."

В приведенном выше примере, approximateCount оценивается в инструкции switch. Каждый кейс сравнивает это значение с числом или интервалом. Поскольку значение approximateCount попадает на диапазон от 12 до 100, naturalCount присваивается значение "dozens of", и исполнение перемещается из инструкции switch.

Кортежи

Вы можете использовать кортежи для тестирования нескольких значений в одной и той же инструкции switch. Каждый элемент кортежа может быть протестирован с любой величиной или с диапазоном величин. Так же вы можете использовать идентификатор подчеркивания (_) для соответствия любой возможной величине.

Пример ниже берет точку с координатами (x, y), выраженную в виде кортежа (Int, Int) и относит к соответствующей категории как следует из примера ниже:

let somePoint = (1, 1)

switch somePoint {

case (0, 0):

print("\(somePoint) is at the origin")

case (_, 0):

print("\(somePoint) is on the x-axis")

case (0, _):

print("\(somePoint) is on the y-axis")

case (-2...2, -2...2):

print("\(somePoint) is inside the box")

default:

print("\(somePoint) is outside of the box")

}

// Выведет "(1, 1) is inside the box"

Инструкции switch определяет: находится ли точка в начале отсчета (0,0), на красной оси x, на оранжевой оси y, внутри синего квадрата 4х4, в котором точка отсчета находится в центре или находится вне этого квадрата.

В отличии от C, инструкция switch в Swift позволяет множественное совпадение или пересечение значений нескольких случаев. Это факт, что точка (0, 0) соответствует всем четырем условиям в этом примере. Однако, если возможно совпадение сразу с несколькими шаблонами, то в расчет принимается только первое из них. То есть точка (0, 0) будет удовлетворять случаю case (0, 0):, а остальные случаи будут проигнорированы.

Привязка значений

Кейс в инструкции switch может связывать значение или значения, с которыми сравнивается, с временными константами или переменными. Это известно как связывание значений, потому что значения "связаны" с временными константами или переменным внутри тела кейса.

Пример ниже берет точку с координатами (x, y), представленной в виде кортежа (Int, Int) и определяет ее позицию на графике, который представлен ниже:

let anotherPoint = (2, 0)

switch anotherPoint {

case (let x, 0):

print("on the x-axis with an x value of \(x)")

case (0, let y):

print("on the y-axis with a y value of \(y)")

case let (x, y):

print("somewhere else at (\(x), \(y))")

}

// Выведет "on the x-axis with an x value of 2

Инструкция switch определяет лежит ли точка на красной оси x или оранжевой оси y, а может быть она не будет ни на одной из осей.

Три случая в инструкции switch объявляют константы x, y, которым временно присваиваются значения одного или обоих элементов из кортежа anotherPoint. В первом кейсе (let x, 0): подойдет любая точка со значением y равным 0, а в константу x запишется значение координаты x нашей точки. Аналогично и во втором случае, когда case (0, let y), этот кейс включает все точки при значении их координаты x равной 0, и происходит присваивание значения координаты y в временную константу y.

Объявленная константа может быть использована внутри блока кейса. Здесь мы их используем как сокращенный вариант для вывода сообщения с помощью функции print.

Заметьте, что инструкция switch не имеет случая default. Последний кейс let (x, y) объявляет кортеж двух констант плейсхолдеров, которые могут соответствовать абсолютно любому значению. Так как anotherPoint это кортеж с двумя значениями, этот кейс подходит под все возможные оставшиеся значения, и кейс default уже не нужен, так как инструкция switch исчерпывающая.

Where

В кейсе инструкции switch мы так же можем использовать дополнительное условие с помощью ключевого слова where.

Пример ниже размещает точку (x, y) на приведенном рисунке:

let yetAnotherPoint = (1, -1)

switch yetAnotherPoint {

case let (x, y) where x == y:

print("(\(x), \(y)) is on the line x == y")

case let (x, y) where x == -y:

print("(\(x), \(y)) is on the line x == -y")

case let (x, y):

print("(\(x), \(y)) is just some arbitrary point")

}

// Выведет "(1, -1) is on the line x == -y"

Инструкция switch определяет лежит ли точка на зеленой диагонали, где x == y, или фиолетовой диагонали, где x == -y, или ни на одной и ни на другой.

Три кейса объявляют константы x, y, которые временно берут значения из кортежа yetAnotherPoint. Эти константы используются как часть условия where, для создания динамического фильтра. Кейс switch совпадает с текущим значением point только в том случае, если условие оговорки where возвращает true для этого значения.

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

Составные кейсы

Если несколько кейсов инструкции switch содержат один и тот же код для исполнения, то шаблоны этих кейсов можно обьединить через запятую после ключевого слова case. Если хотя бы один из шаблонов кейса соответствует сравниваемому значению, то значит и сам кейс соответствует сравниваемому значению. Шаблоны в кейсе могут быть записаны на несколько строк, если их очень много. Например:

let someCharacter: Character = "e"

switch someCharacter {

case "a", "e", "i", "o", "u":

print("\(someCharacter) is a vowel")

case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",

"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":

print("\(someCharacter) is a consonant")

default:

print("\(someCharacter) is not a vowel or a consonant")

}

// Напечатает "e is a vowel"

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

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

let stillAnotherPoint = (9, 0)

switch stillAnotherPoint {

case (let distance, 0), (0, let distance):

print("On an axis, \(distance) from the origin")

default:

print("Not on an axis")

}

// Напечатает "On an axis, 9 from the origin"

Кейс выше имеет два шаблона: (let distance, 0), который соответсвует любой точке на оси x, и (0, let distance), что соответствует точке на оси y. И тот и другой шаблон включают в себя привязку для distance и distance является целочисленным значением для двух этих шаблонов, что значит, что код внутри тела кейса всегда будет иметь доступ к значению distance.

Операторы передачи управления

Операторы передачи управления меняют последовательность исполнения вашего кода, передавая управление от одного фрагмента кода другому. В Swift есть пять операторов передачи управления:

continue

break

fallthrough

return

throw

Операторы continue, break, fallthrough будут описаны в следующих главах, оператор return будет описан в главе Функции, а оператор throw будет описан в Передача ошибки с помощью генерирующей функции.

Оператор Continue

Оператор continue говорит циклу прекратить текущую итерацию и начать новую. Он как бы говорит: "Я закончил с текущей итерацией", но полностью из цикла не выходит.

Следующий пример убирает все пробелы и гласные в нижнем регистре из строки, для того чтобы создать загадочную фразу-головоломку:

let puzzleInput = "great minds think alike"

var puzzleOutput = ""

let charactersToRemove: [Character] = ["a", "e", "i", "o", "u", " "]

for character in puzzleInput {

if charactersToRemove.contains(character) {

continue

} else {

puzzleOutput.append(character)

}

}

print(puzzleOutput)

// Выведет "grtmndsthnklk"

Пример выше вызывает оператор continue, когда он находит соответствие с гласными звуками или пробелом, вызывая тем самым прекращение текущей итерации и начало новой.

Оператор Break

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

Оператор Break в цикле

Когда оператор break используется внутри цикла, то он немедленно прекращает работу цикла, и выполнение кода продолжается с первой строки после закрывающей скобки цикла (}). Никакой последующий код из текущей итерации цикла выполняться не будет, и никакие дальнейшие итерации цикла не будут запускаться.

Оператор Break в инструкции Switch

Когда оператор break используется внутри инструкции switch, то он прекращает исполнение кода конкретного случая и перекидывает исполнение на первую строку после закрывающей скобки (}) инструкции switch.

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

Заметка

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

Следующий пример переключается на символьные значение Character и определяет, является ли символ целым числом на одном из четырех языков. Несколько языков включены в каждый кейс для краткости:

let numberSymbol: Character = "三" // Цифра 3 в упрощенном Китайском языке

var possibleIntegerValue: Int?

switch numberSymbol {

case "1", "١", "一", "๑":

possibleIntegerValue = 1

case "2", "٢", "二", "๒":

possibleIntegerValue = 2

case "3", "٣", "三", "๓":

possibleIntegerValue = 3

case "4", "٤", "四", "๔":

possibleIntegerValue = 4

default:

break

}

if let integerValue = possibleIntegerValue {

print("The integer value of \(numberSymbol) is \(integerValue).")

} else {

print("An integer value could not be found for \(numberSymbol).")

}

// Выведет "The integer value of 三 is 3."

Этот пример проверяет numberSymbol на наличие в нем целого числа от 1 до 4 на арабском, латинском, китайском или тайском языках. Если совпадение найдено, то один из кейсов switch устанавливает опциональную переменную Int?, названную possibleIntegerValue в подходящее целочисленное значение.

После того как инструкция switch выполнена, пример использует опциональную привязку для определения наличия величины. Переменная possibleIntegerValue имеет неявное начальное значение равное nil в силу того, что она имеет опциональный тип, таким образом опциональная привязка пройдет успешно только в том случае, если possibleIntegerValue будет иметь актуальное значение одного из четырех первых кейсов инструкции switch.

Так как в примере выше не практично перечислять каждое возможное значение Character, то кейс default улавливает все остальные варианты символов, которые не соответствуют первым четырем кейсам. Кейсу default не надо предпринимать какие-либо действия, так что там прописан только оператор break. После того как срабатывает кейс default, срабатывает и break, что прекращает действие инструкции switch и код продолжает свою работу с if let.

Оператор Fallthrough

Инструкция switch в Swift не проваливается из каждого кейса в следующий. Напротив, как только находится соответствие с первым кейсом, так сразу и прекращается работа всей инструкции. А в языке C, работа инструкции switch немного сложнее, так как требует явного прекращения работы при нахождении соответствия словом break в конце кейса, в противном случае при соответствии мы провалимся в следующий случай и так далее пока не встретим слово break. Избежание провалов значит что инструкция switch в Swift более краткая и предсказуемая, чем она же в C, так как она предотвращает срабатывание нескольких кейсов по ошибке.

Если вам по какой-то причине нужно аналогичное проваливание как в C, то вы можете использовать оператор fallthrough в конкретном кейсе. Пример ниже использует fallthrough для текстового описания целого числа:

let integerToDescribe = 5

var description = "The number \(integerToDescribe) is"

switch integerToDescribe {

case 2, 3, 5, 7, 11, 13, 17, 19:

description += " a prime number, and also"

fallthrough

default:

description += " an integer."

}

print(description)

// Выведет "The number 5 is a prime number, and also an integer."

В примере мы объявляем новую переменную типа String, названную description и присваиваем ей исходное значение. Потом мы определяем величину integerToDescribe, используя инструкцию switch. Если значение integerToDescribe одно из значений списка кейса, то мы получаем текстовое описание значения, которое дополняется значением, которое находится в default, так как на уровень выше в сработавшем кейсе стоит ключевое слово fallthrough, после чего завершается работа инструкции switch.

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

После того как сработала инструкция switch, мы получаем описание числа, используя функцию print(_:separator:terminator:). В нашем примере мы получаем 5, что корректно определено как простое число.

Заметка

Ключевое слово fallthrough не проверяет условие кейса, оно позволяет провалиться из конкретного кейса в следующий или в default, что совпадает со стандартным поведением инструкции switch в языке C.

Маркированные инструкции

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

Для всех этих целей мы можем маркировать цикл или инструкцию switch маркером инструкций и использовать его вместе с оператором break или оператором continue для предотвращения или продолжения исполнения маркированной инструкции.

Маркированные инструкции обозначаются меткой в той же строке, что и ключевое слово начала инструкции, которое следует после метки через двоеточие. Ниже приведен пример синтаксиса цикла while, хотя принцип работы маркера такой же со всеми инструкциями:

имя маркера : while условие {

исполняемый код

}

В дальнейшем примере мы будем использовать break, continue с маркированным циклом while для адаптированной версии Змеи и Лестницы, которую вы видели ранее в "Циклы While". В этот раз у нас появилось новое правило:

Чтобы победить вы должны попасть точно на клетку 25.

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

Игровая зона доски осталась такой же как и была:

Величины finalSquare, board, square и diceRoll инициализируются точно так же как и в прошлых примерах игры:

let finalSquare = 25

var board = [Int](repeating: 0, count: finalSquare + 1)

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02

board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

var square = 0

var diceRoll = 0

В этой версии игры используется цикл while и инструкция switch для воплощения логики игры. Цикл while маркер названный gameLoop, для индикации главного цикла игры.

Условие цикла while square != finalSquare показывает, что теперь нам нужно попасть строго на клетку 25:

gameLoop: while square != finalSquare {

diceRoll += 1

if diceRoll == 7 { diceRoll = 1 }

switch square + diceRoll {

case finalSquare:

//после броска кубика мы попадаем на клетку 25, игра окончена

break gameLoop

case let newSquare where newSquare > finalSquare:

//мы кинули кубик на слишком большое значение, значит нам нужно кинуть снова

continue gameLoop

default:

//допустимое движение по игровому полю, двигаемся

square += diceRoll

square += board[square]

}

}

print("Game over!")

Игральная кость бросается в начале каждого цикла. Прежде чем двигаться по доске идет проверка в инструкции switch на валидность хода, потом обрабатывает его, если такое движение допустимо:

Если игральная кость двигает игрока на последнюю клетку, игра заканчивается. Оператор break с маркером gameLoop перекидывает исполнение кода на первую строку кода после цикла while, которая и завершает игру.

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

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

Заметка

Если оператор break не использует маркер gameLoop, то он будет прерывать выполнение инструкции switch, а не всего цикла while. Но используя маркер gameLoop мы можем указать какое исполнение инструкции нужно прервать.

Обратите внимание так же и на то, что нет необходимости использовать маркер gameLoop, когда мы обращаемся к continue gameLoop для того, чтобы перейти к следующей итерации. В этой игре всего один цикл, так что нет никакой двусмысленности на какой цикл этот оператор может вообще воздействовать. Однако и никакого вреда нет в том, что мы явно указали gameLoop маркер в строке с оператором continue. Более того, делая так мы делаем наш код нагляднее и восприятие логики игры становится более ясным.

Ранний выход

Инструкция guard, как и инструкция if, выполняет выражения в зависимости от логического значения условия. Используйте guard, чтобы указать на то, что условие обязательно должно быть true, чтобы код после самой инструкции guard выполнился. В отличии от инструкции if, guard всегда имеет код внутри else, который выполняется, когда условие оценивается как false.

func greet(person: [String: String]) {

guard let name = person["name"] else {

return

}

print("Привет \(name)!")

guard let location = person["location"] else {

print("Надеюсь у тебя там хорошая погода.")

return

}

print("Надеюсь в \(location) хорошая погода.")

}

greet(person: ["name": "John"])

// Выведет "Привет John!"

// Выведет "Надеюсь у тебя там хорошая погода."

greet(person: ["name": "Jane", "location": "Cupertino"])

// Выведет "Привет Jane!"

// Выведет "Надеюсь в Cupertino хорошая погода.”

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

Если условие не выполняется, то исполняется код внутри else. Эта ветка должна перебросить исполнение кода на выход из этого блока кода, в котором был определен guard. А сделать это можно при помощи инструкций return, break, continue, throw или можно вызвать метод, который ничего не возвращает, например fatalError(_file:line:).

Использование инструкции guard для каких-либо требований улучшает читабельность кода по сравнению с if. Он помогает вам написать код, который вам не нужно будет помещать в блок else и позволит вам держать код, который обрабатывает нарушение требований рядом с самими требованиями.

Проверка доступности API

В Swift есть встроенная поддержка для проверки доступности API, благодаря которой вы будете уверены, что не используете  API-интерфейсы, недоступные для данной deployment target.

Компилятор использует информацию о доступности в SDK, чтобы убедиться, что все API-интерфейсы, используемые в коде, доступны для deployment target, указанного в вашем проекте. Swift выдает сообщение об ошибке во время компиляции, если вы пытаетесь использовать недоступный API.

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

if #available(iOS 10, macOS 10.12, *) {

// Используйте API iOS 10 для iOS и используйте API macOS 10.12 на macOS

} else {

// Используйте более старые API для iOS и macOS

}

Условие доступности выше указывает, что на iOS тело if выполняется только на iOS 10 и более поздних версиях; что касается macOS: только на macOS 10.12 и более поздних версиях. Последний аргумент, *, требует и указывает, что на любой другой платформе, тело if выполняется на минимальной указанной deployment target.

В общем виде условие доступности принимает список названий платформ и версий. Вы можете использовать названия платформы, такие как iOS, macOS, watchOS, и tvOS; полный список можно найти в Атрибуты объявлений. В дополнение к определению основных номеров версий, такие как iOS 8 или macOs 10.10, вы можете указать второстепенные версии номера, такие как iOS 8.3 и macOS 10.10.3.

if #available (название платформы версия платформы, ..., * ) {

выражения для исполнения, если соответствующие условию API доступны

} else {

выражения для исполнения, если соответствующие условию API не доступны

}

Функции

Функции – это самостоятельные фрагменты кода, решающие определенную задачу. Каждой функции присваивается уникальное имя, по которому ее можно идентифицировать и "вызвать" в нужный момент.

Язык Swift предлагает достаточно гибкий единый синтаксис функций – от простых C-подобных функций без параметров до сложных методов в стиле Objective-C с локальными и внешними параметрами. Параметры могут служить как для простой инициализации значений внутри функции, так и для изменения внешних переменных после выполнения функции.

Каждая функция в Swift имеет тип, описывающий тип параметров функции и тип возвращаемого значения. Тип функции можно использовать аналогично любым другим типам в Swift, т. е. одна функция может быть параметром другой функции либо ее результирующим значением. Функции также могут вкладываться друг в друга, что позволяет инкапсулировать определенный алгоритм внутри локального контекста.

Объявление и вызов функций

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

У каждой функции должно быть имя, которое отражает решаемую задачу. Чтобы воспользоваться функцией, ее нужно "вызвать", указав имя и входные значения (аргументы), соответствующие типам параметров этой функции. Аргументы функции всегда должны идти в том же порядке, в каком они были указаны при объявлении функции.

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

func greet(person: String) -> String {

let greeting = "Hello, " + person + "!"

return greeting

}

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

Из объявления функции можно узнать, что она делает, какие у нее входные данные и какой результат она возвращает. Объявленную функцию можно однозначно вызывать из любого участка кода:

print(greet(person: "Anna"))

// Prints "Hello, Anna!"

print(greet(person: "Brian"))

// Prints "Hello, Brian!"

Функция greet(person:) вызывается, принимая значение типа String, которое стоит после имени person, например вот так - greet(person: "Anna"). Поскольку функция возвращает значение типа String, вызов функции greet(person:) может быть завернут в вызов для функции print(_:separator:terminator:), чтобы напечатать полученную строку и увидеть возвращаемое значение (см. выше).

Заметка

У print(_:separator:terminator:) нет ярлыка для первого аргумента, и его другие аргументы являются опциональными, поскольку имеют дефолтное значение (по умолчанию). Эти вариации синтаксиса функции рассматриваются ниже в Ярлыки аргументов и имена параметров функций и Значения по умолчанию для параметров.

Тело функции greet(person:) начинается с объявления новой константы типа String под названием greeting, и устанавливается простое сообщение-приветствие. Затем это приветствие возвращается в точку вызова функции с помощью ключевого слова return. После выполнения оператора return greeting функция завершает свою работу и возвращает текущее значение greeting.

Функцию greet(person:) можно вызывать многократно и с разными входными значениями. В примере выше показано, что будет, если функцию вызвать с аргументом "Anna" и со значением "Brian". В каждом случае функция возвратит персональное приветствие.

Чтобы упростить код этой функции, можно записать создание сообщения и его возврат в одну строку:

func greetAgain(person: String) -> String {

return "Hello again, " + person + "!"

}

print(greetAgain(person: "Anna"))

// Выведет "Hello again, Anna!"

Параметры функции и возвращаемые значения

В языке Swift параметры функций и возвращаемые значения реализованы очень гибко. Разработчик может объявлять любые функции – от простейших, с одним безымянным параметром, до сложных, со множеством параметров и составными именами.

Функции без параметров

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

func sayHelloWorld() -> String {

return "hello, world"

}

print(sayHelloWorld())

// Выведет "hello, world"

Обратите внимание, что несмотря на отсутствие параметров, в объявлении функции все равно нужно ставить скобки после имени. При вызове после имени функции также указываются пустые скобки.

Функции с несколькими входными параметрами

У функции может быть несколько параметров, которые указываются через запятую в скобках.

Эта функция принимает два параметра: имя человека и булево значение, приветствовали ли его уже, и возвращает соответствующее приветствие для этого человека:

func greet(person: String, alreadyGreeted: Bool) -> String {

if alreadyGreeted {

return greetAgain(person: person)

} else {

return greet(person: person)

}

}

print(greet(person: "Tim", alreadyGreeted: true))

// Выведет "Hello again, Tim!"

Вы вызываете функцию greet(person:alreadyGreeted:), передавая значение типа String параметру с ярлыком person и булево значение с ярлыком alreadyGreeted, взятое в скобки через запятую. Обратите внимание, что эта функция отличается от функции greet(person:), которую вы видели в предыдущем разделе. Хотя имена обеих функций начинаются с greet, функция greet(person:alreadyGreeted:) принимает два аргумента, а greet(person:) принимает только один.

Функции, не возвращающие значения

В некоторых случаях функции могут не иметь возвращаемого типа. Вот другая реализация функции greet(person:), которая выводит свое собственное значение типа String, но не возвращает его:

func greet(person: String) {

print("Hello, \(person)!")

}

greet(person: "Dave")

// Выведет "Hello, Dave!"

Так как у функции нет выходного значения, в ее объявлении отсутствует результирующая стрелка (->) и возвращаемый тип.

Заметка

Строго говоря, функция greet(person:) все же возвращает значение, хотя оно нигде и не указано. Функции, для которых не задан возвращаемый тип, получают специальный тип Void. По сути, это просто пустой кортеж, т. е. кортеж с нулем элементов, который записывается как ().

Выходное значение функции может быть игнорировано:

func printAndCount(string: String) -> Int {

print(string)

return string.count

}

func printWithoutCounting(string: String) {

let _ = printAndCount(string: string)

}

printAndCount(string: "hello, world")

// Выведет "hello, world" и возвращает значение 12

printWithoutCounting(string: "hello, world")

// Выведет "hello, world", но не возвращает значения

Первая функция, printAndCount(string: выводит строку, а затем возвращает подсчет символов в виде целого (Int). Вторая функция, printWithoutCounting(string:) вызывает первую, но игнорирует ее возвращаемое значение. При вызове второй функции первая функция по-прежнему печатает сообщение, но ее возвращаемое значение не используется.

Заметка

Хотя возвращаемые значения можно игнорировать, функция все же должна возвратить то, что задано в ее объявлении. Функция, для которой указан возвращаемый тип, не может заканчиваться оператором, который ничего не возвращает, иначе произойдет ошибка во время компиляции.

Функции, возвращающие несколько значений

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

В следующем примере объявлена функция minMax(array:), которая ищет минимальный и максимальный элементы в массиве типа Int:

func minMax(array: [Int]) -> (min: Int, max: Int) {

var currentMin = array[0]

var currentMax = array[0]

for value in array[1..<array.count] {

if value < currentMin {

currentMin = value

} else if value > currentMax {

currentMax = value

}

}

return (currentMin, currentMax)

}

Функция minMax(array:) возвращает кортеж из двух значений типа Int. Этим значениям присвоены имена min и max, чтобы к ним можно было обращаться при запросе возвращаемого типа функции.

Тело функции minMax(array:) начинается с инициализации двух рабочих переменных currentMin и currentMax значением первого целого элемента в массиве. Затем функция последовательно проходит по всем остальным значениям в массиве и сравнивает их со значениями currentMin и currentMax соответственно. И наконец, самое маленькое и самое большое значения возвращаются внутри кортежа типа Int.

Так как имена элементов кортежа указаны в возвращаемом типе функции, к ним можно обращаться через точку и считывать значения:

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])

print("min is \(bounds.min) and max is \(bounds.max)")

// Выведет "min is -6 and max is 109"

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

Опциональный кортеж как возвращаемый тип

Если возвращаемый из функции кортеж может иметь "пустое значение", то его следует объявить как опциональный кортеж, т. е. кортеж, который может равняться nil. Чтобы сделать возвращаемый кортеж опциональным, нужно поставить вопросительный знак после закрывающей скобки:(Int, Int)? или (String, Int, Bool)?.

Заметка

Кортеж-опционал вида (Int, Int)? это не то же самое, что кортеж, содержащий опционалы: (Int?, Int?). Кортеж-опционал сам является опционалом, но не обязан состоять из опциональных значений.

Функция minMax(array:) выше возвращает кортеж из двух значений типа Int, однако не проверяет корректность передаваемого массива. Если аргумент array содержит пустой массив, для которого count равно 0, функция minMax в том виде, в каком она приведена выше, выдаст ошибку выполнения, когда попытается обратиться к элементу array[0].

Для устранения этого недочета перепишем функцию minMax(array:) так, чтобы она возвращала кортеж-опционал, который в случае пустого массива примет значение nil:

func minMax(array: [Int]) -> (min: Int, max: Int)? {

if array.isEmpty { return nil }

var currentMin = array[0]

var currentMax = array[0]

for value in array[1..<array.count] {

if value < currentMin {

currentMin = value

} else if value > currentMax {

currentMax = value

}

}

return (currentMin, currentMax)

}

Чтобы проверить, возвращает ли эта версия функции minMax(array:) фактическое значение кортежа или nil, можно использовать привязку опционала:

if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {

print("min is \(bounds.min) and max is \(bounds.max)")

}

// Выведет "min is -6 and max is 109"

Ярлыки аргументов и имена параметров функций

Каждый параметр функции имеет ярлык аргумента и имя параметра. Ярлык аргумента используется при вызове функции. Каждый параметр при вызове функции записывается  с ярлыком аргумента, стоящим перед ним. Имя параметра используется при реализации функции. По умолчанию параметры используют имена их параметров в качестве ярлыка аргумента.

func someFunction(firstParameterName: Int, secondParameterName: Int) {

// Внутри тела функции firstParameterName и secondParameterName

// ссылаются на значения аргументов, первого и второго параметров.

}

someFunction(firstParameterName: 1, secondParameterName: 2)

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

Указываем ярлыки аргументов

Вы пишите ярлык аргумента перед именем параметра через пробел:

func someFunction(argumentLabel parameterName: Int) {

// В теле функции parameterName относится к значению аргумента

// для этого параметра.

}

Вот вариант функции greet(person:), которая принимает имя человека и его родной город, затем возвращает приветствие:

func greet(person: String, from hometown: String) -> String {

return "Hello \(person)! Glad you could visit from \(hometown)."

}

print(greet(person: "Bill", from: "Cupertino"))

// Выводит "Hello Bill! Glad you could visit from Cupertino."

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

Пропуск ярлыков аргумента

Если вы не хотите использовать ярлык аргумента в качестве параметра, используйте нижнее подчеркивание (_) вместо явного ярлыка аргумента для этого параметра.

func someFunction(_ firstParameterName: Int, secondParameterName: Int) {

// В теле функции firstParameterName и secondParameterName

// ссылаются на значения аргументов для первого и второго параметров.

}

someFunction(1, secondParameterName: 2)

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

Значения по умолчанию для параметров

При объявлении функции любому из ее параметров можно присвоить значение по умолчанию. Если у параметра есть значение по умолчанию, то при вызове функции этот параметр можно опустить.

func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {

// Если вы пропускаете второй аргумент при вызове функции, то

// значение parameterWithDefault будет равняться 12 внутри тела функции.

}

someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault равен 6

someFunction(parameterWithoutDefault: 4) // parameterWithDefault равен 12

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

Вариативные параметры

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

Значения, переданные через вариативный параметр, доступны внутри функции в виде массива соответствующего типа. Например, вариативный параметр numbers типа Double... доступен внутри функции в виде массива-константы numbers типа [Double].

В приведенном ниже примере вычисляется среднее арифметическое (или же среднее) последовательности чисел, имеющей произвольную длину:

func arithmeticMean(_ numbers: Double...) -> Double {

var total: Double = 0

for number in numbers {

total += number

}

return total / Double(numbers.count)

}

arithmeticMean(1, 2, 3, 4, 5)

// возвращает 3.0, что является средним арифметическим этих пяти чисел

arithmeticMean(3, 8.25, 18.75)

// возвращает 10.0, что является средним арифметическим этих трех чисел

Заметка

У функции может быть только один вариативный параметр.

Сквозные параметры

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

Для создания сквозного параметра нужно поставить ключевое слово inout перед объявлением параметра. Сквозной параметр передает значение в функцию, которое затем изменяется в ней и возвращается из функции, заменяя исходное значение. Более подробную информацию поведения сквозных параметров и связанных с ними оптимизаций компилятора см. Сквозные Параметры.

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

Заметка

Сквозные параметры не могут иметь значения по умолчанию, а вариативные параметры не могут быть сквозными, с ключевым словом inout.

Вот пример функции под названием swapTwoInts(_:_:), у которой есть два сквозных целочисленных параметра – a и b:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {

let temporaryA = a

a = b

b = temporaryA

}

Функция swapTwoInts(_:_:) просто меняет значение переменной b на значение a, а значение a – на значение b. Для этого функция сохраняет значение a во временной переменной temporaryA, присваивает значение b переменной a, а затем присваивает значение temporaryA переменной b.

Вы можете вызвать функцию swapTwoInts (_: _:) с двумя переменными типа Int, чтобы поменять их значения. Обратите внимание, что имена someInt и anotherInt начинаются с амперсанда, когда они передаются в swapTwoInts (_: _:) функции:

var someInt = 3

var anotherInt = 107

swapTwoInts(&someInt, &anotherInt)

print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")

// Выведет "someInt is now 107, and anotherInt is now 3"

В вышеприведенном примере видно, что исходные значения переменных someInt и anotherInt изменены функцией swapTwoInts (_: _:), несмотря на то, что изначально они были объявлены за ее пределами.

Заметка

Сквозные параметры – это не то же самое, что возвращаемые функцией значения. В примере с функцией swapTwoInts нет ни возвращаемого типа, ни возвращаемого значения, но параметры someInt и anotherInt все равно изменяются. Сквозные параметры – это альтернативный способ передачи изменений, сделанных внутри функции, за пределы тела этой функции.

Функциональные типы

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

Пример:

func addTwoInts(a: Int, _ b: Int) -> Int {

return a + b

}

func multiplyTwoInts(a: Int, _ b: Int) -> Int {

return a * b

}

В данном примере объявлены две простые математические функции – addTwoInts и multiplyTwoInts. Каждая из этих функций принимает два значения типа Int и возвращает одно значение типа Int, содержащее результат математической операции.

Обе функции имеют тип (Int, Int) -> Int. Эта запись означает следующее:

"функция с двумя параметрами типа Int, возвращающая значение типа Int".

Вот еще один пример, но уже функции без параметров и возвращаемого значения:

func printHelloWorld() {

print("hello, world")

}

Эта функция имеет тип () -> Void, т. е. "функция без параметров, которая возвращает Void".

Использование функциональных типов

В Swift с функциональными типами можно работать так же, как и с другими типами. Например, можно объявить константу или переменную функционального типа и присвоить ей функцию соответствующего типа:

var mathFunction: (Int, Int) -> Int = addTwoInts

Эта запись означает следующее:

"Объявить переменную mathFunction, имеющую тип "функция, принимающая два значения типа Int, и возвращающая одно значение типа Int". Присвоить этой новой переменной указатель на функцию addTwoInts".

Функция addTwoInts имеет тот же тип, что и переменная mathFunction, поэтому с точки зрения языка Swift такое присваивание корректно.

Теперь функцию можно вызывать с помощью переменной mathFunction:

print("Result: \(mathFunction(2, 3))")

// Выведет "Result: 5"

Той же переменной можно присвоить и другую функцию такого же типа – аналогично нефункциональным типам:

mathFunction = multiplyTwoInts

print("Result: \(mathFunction(2, 3))")

// Выведет "Result: 6"

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

let anotherMathFunction = addTwoInts

// для переменной anotherMathFunction выведен тип (Int, Int) -> Int

Функциональные типы как типы параметров

Функциональные типы наподобие (Int, Int) -> Int могут быть типами параметров другой функции. Это позволяет определять некоторые аспекты реализации функции непосредственно во время ее вызова.

Следующий код печатает на экране результаты работы приведенных выше математических функций:

func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {

print("Result: \(mathFunction(a, b))")

}

printMathResult(addTwoInts, 3, 5)

// Выведет "Result: 8"

В этом примере объявлена функция printMathResult(_:_:_:), у которой есть три параметра. Первый параметр под названием mathFunction имеет тип (Int, Int) -> Int. Соответственно, аргументом этого параметра может быть любая функция такого же типа. Второй и третий параметры называются a и b и относятся к типу Int. Они служат для передачи двух входных значений для математической функции.

При вызове printMathResult(_:_:_:) получает в качестве входных данных функцию addTwoInts(_:_:) и два целочисленных значения 3 и 5. Затем она вызывает переданную функцию со значениями 3 и 5, а также выводит на экран результат 8.

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

Функциональные типы как возвращаемые типы

Функциональный тип можно сделать возвращаемым типом другой функции. Для этого нужно записать полный функциональный тип сразу же после возвратной стрелки (->) в возвращаемой функции.

В следующем примере объявлены две простые функции – stepForward(_:) и stepBackward(_:). Функция stepForward(_:) возвращает входное значение, увеличенное на единицу, а функция stepBackward(_:) – уменьшенное на единицу. Обе функции имеют тип (Int) -> Int:

func stepForward(_ input: Int) -> Int {

return input + 1

}

func stepBackward(_ input: Int) -> Int {

return input - 1

}

Следующая функция под названием chooseStepFunction(backward:) имеет возвращаемый (Int) -> Int. Функция chooseStepFunction(backward:) возвращает функцию stepForward(_:) или функцию stepBackward(_:) в зависимости от значения логического параметра backward:

func chooseStepFunction(backward: Bool) -> (Int) -> Int {

return backward ? stepBackward : stepForward

}

Теперь с помощью chooseStepFunction(backward:) можно получать функцию, которая будет сдвигать значение влево или вправо:

var currentValue = 3

let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)

// moveNearerToZero now refers to the stepBackward() function

В предыдущем примере мы определяли, нужно ли прибавить или отнять единицу, чтобы последовательно приблизить переменную currentValue к нулю. Изначально currentValue имеет значение 3, т. е. сравнение currentValue > 0 даст true, а функция chooseStepFunction(backward:), соответственно, возвратит функцию stepBackward(_:). Указатель на возвращаемую функцию хранится в константе moveNearerToZero.

Так как moveNearerToZero теперь ссылается на нужную функцию, можно использовать эту константу для отсчета до нуля:

print("Counting to zero:")

// Counting to zero:

while currentValue != 0 {

print("\(currentValue)... ")

currentValue = moveNearerToZero(currentValue)

}

print("zero!")

// 3...

// 2...

// 1...

// zero!

Вложенные функции

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

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

Приведенный выше пример с функцией chooseStepFunction(backward:) можно переписать со вложенными функциями:

func chooseStepFunction(backward: Bool) -> (Int) -> Int {

func stepForward(input: Int) -> Int { return input + 1 }

func stepBackward(input: Int) -> Int { return input - 1 }

return backward ? stepBackward : stepForward

}

var currentValue = -4

let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)

// moveNearerToZero теперь ссылается на вложенную функцию stepForward()

while currentValue != 0 {

print("\(currentValue)... ")

currentValue = moveNearerToZero(currentValue)

}

print("zero!")

// -4...

// -3...

// -2...

// -1...

// zero!

Замыкания

Замыкания - это самодостаточные блоки с определенным функционалом, которые могут быть переданы и использованы в вашем коде. Замыкания в Swift похожи на блоки в C и Objective-C, и лямбды в других языках программирования.

Замыкания могут захватывать и хранить отношения для любых констант и переменных из контекста, в котором они объявлены. Эта процедура известна как заключение этих констант и переменных, отсюда и название "замыкание". Swift выполняет всю работу с управлением памятью при захвате за вас.

Заметка

Не волнуйтесь, если вы не знакомы с понятием "захвата"(capturing). Это объясняется более подробно ниже в главе Захват значений.

Глобальные и вложенные функции, которые были представлены в главе Функции, являются частным случаем замыканий. Замыкания принимают одну из трех форм:

Глобальные функции являются замыканиями, у которых есть имя и которые не захватывают никакие значения.

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

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

Выражения замыкания в Swift имеют четкий, ясный, оптимизированный синтаксис в распространенных сценариях. Эти оптимизации включают:

Вывод типа параметра и возврат типа значения из контекста

Неявные возвращающиеся значения однострочных замыканий

Сокращенные имена параметров

Синтаксис последующих замыканий

Замыкающие выражения

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

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

Метод sorted

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

Примеры замыкающих выражений ниже используют метод sorted(by:)для сортировки массива из String значений в обратном алфавитном порядке. Вот исходный массив для сортировки:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

Замыкание метода sorted(by:) принимает два аргумента одного и того же типа, что и содержимое массива, и возвращает Bool значение, которое решает поставить ли первое значение перед вторым, или после второго. Замыкание сортировки должно вернуть true, если первое значение должно быть до второго значения, и false в противном случае.

Этот пример сортирует массив из String значений, так что сортирующее замыкание должно быть функцией с типом (String, String) -> Bool.

Один из способов обеспечить сортирующее замыкание, это написать нормальную функцию нужного типа, и передать его в качестве аргумента метода sorted(by:):

func backward(_ s1: String, _ s2: String) -> Bool {

return s1 > s2

}

var reversedNames = names.sorted(by: backward)

// reversedNames равен ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

Если первая строка (s1) больше чем вторая строка (s2), функция backwards(_:_:) возвращает true, что указывает, что s1 должна быть перед s2 в сортированном массиве. Для символов в строках, "больше чем" означает "появляется в алфавите позже, чем". Это означает что буква "B" "больше чем" буква "А", а строка "Tom" больше чем строка "Tim". Это делает обратную алфавитную сортировку, с "Barry" поставленным перед "Alex", и так далее.

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

Синтаксис замыкающего выражения

Синтаксис замыкающего выражения имеет следующую общую форму:

{ (параметры) -> тип результата in

выражения

}

Синтаксис замыкающего выражения может использовать сквозные параметры. Значения по умолчанию не могут быть переданы. Вариативные параметры могут быть использованы в любом месте в списке параметров. Кортежи также могут быть использованы как типы параметров и как типы возвращаемого значения.

Пример ниже показывает версию функции backwards(_:_:) с использованием замыкающего выражения:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in

return s1 > s2

})

Обратите внимание, что объявление типов параметров и типа возвращаемого значения для этого однострочного замыкания идентично объявлению из функции backwards(_:_:). В обоих случаях, оно пишется в виде (s1: String, s2: String) -> Bool. Тем не менее, для однострочных замыкающих выражений, параметры и тип возвращаемого значения пишутся внутри фигурных скобок, а не вне их.

Начало тела замыкания содержит ключевое слово in. Это ключевое слово указывает, что объявление параметров и возвращаемого значения замыкания закончено, и тело замыкания вот-вот начнется.

Поскольку тело замыкания настолько короткое, оно может быть записано в одну строку:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })

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

Определение типа из контекста

Поскольку сортирующее замыкание передается как аргумент метода, Swift может вывести типы его параметров и тип возвращаемого значения, через тип параметра метода sorted(by:). Этот параметр ожидает функцию имеющую тип (String, String) -> Bool. Это означает что типы (String, String) и Bool не нужно писать в объявлении замыкающего выражения. Поскольку все типы могут быть выведены, стрелка результата ( -> ) и скобки вокруг имен параметров также могут быть опущены:

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

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

Тем не менее, вы всё равно можете явно указать типы, если хотите. И делать это предполагается, если это поможет избежать двусмысленности для читателей вашего кода. В случае с методом sorted(by:), цель замыкания понятна из того факта, что сортировка происходит, и она безопасна для читателя, который может предположить, что замыкание, вероятно, будет работать со значениями String, поскольку оно помогает сортировать массив из строк.

Неявные возвращаемые значения из замыканий с одним выражением

Замыкания с одним выражением могут неявно возвращать результат своего выражения через опускание ключевого слова return из их объявления, как показано в этой версии предыдущего примера:

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })

Здесь, функциональный тип аргумента метода sorted(by:)дает понять, что замыкание вернет Bool значение. Поскольку тело замыкания содержит одно выражение (s1 > s2), которое возвращает Bool значение, то нет никакой двусмысленности, и ключевое слово return можно опустить.

Сокращенные имена параметров

Swift автоматически предоставляет сокращённые имена для однострочных замыканий, которые могут быть использованы для обращения к значениям параметров замыкания через имена $0, $1, $2, и так далее.

Если вы используете эти сокращенные имена параметров с вашим замыкающим выражением, вы можете пропустить список параметров замыкания из его объявления, а количество и тип сокращенных имен параметров будет выведено из ожидаемого типа метода. Ключевое слово in также может быть опущено, поскольку замыкающее выражение полностью состоит из его тела:

reversedNames = names.sorted(by: { $0 > $1 })

Здесь, $0 и $1 обращаются к первому и второму String параметру замыкания.

Операторные функции

Здесь есть на самом деле более короткий способ написать замыкающее выражение выше. Тип String в Swift определяет свою специфичную для строк реализацию оператора больше ( > ) как функции, имеющей два строковых параметра и возвращающей значение типа Bool. Это точно соответствует типу метода, для параметра метода sorted(by:). Таким образом, вы можете просто написать оператор больше, а Swift будет считать, что вы хотите использовать специфичную для строк реализацию:

reversedNames = names.sorted(by: >)

Более подробную информацию о операторных функциях смотрите в разделе Операторные функции.

Последующее замыкание

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

func someFunctionThatTakesAClosure(closure: () -> Void) {

// тело функции

}

// Вот как вы вызываете эту функцию без использования последующего замыкания:

someFunctionThatTakesAClosure(closure: {

// тело замыкания

})

// Вот как вы вызываете эту функцию с использованием последующего замыкания:

someFunctionThatTakesAClosure() {

// тело последующего замыкания

}

Сортирующее строки замыкание из раздела Синтаксис замыкающего выражения может быть записано вне круглых скобок функции sorted(by:), как последующее замыкание:

reversedNames = names.sorted() { $0 > $1 }

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

reversedNames = names.sorted { $0 > $1 }

Последующие замыкания полезны в случаях, когда само замыкание достаточно длинное, и его невозможно записать в одну строку. В качестве примера приведем вам метод map(_:) типа Array в языке Swift, который принимает выражение замыкания как его единственный аргумент. Замыкание вызывается по одному разу для каждого элемента массива и возвращает альтернативную отображаемую величину (возможно другого типа) для этого элемента. Природа отображения и тип возвращаемого значения определяется замыканием.

После применения замыкания к каждому элементу массива, метод map(_:) возвращает новый массив, содержащий новые преобразованные величины, в том же порядке, что и в исходном массиве.

Вот как вы можете использовать метод map(_:) вместе с последующим замыканием для превращения массива значений типа Int в массив типа String. Массив [16, 58, 510] используется для создания нового массива ["OneSix", "FiveEight", "FiveOneZero"] :

let digitNames = [

0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",

5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"

]

let numbers = [16, 58, 510]

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

Вы можете использовать массив numbers для создания значений типа String, передав замыкающее выражение в метод map(_:) массива в качестве последующего замыкания. Обратите внимание, что вызов number.map не включает в себя скобки после map, потому что метод map(_:) имеет только один параметр, который мы имеем в виде последующего замыкания:

let strings = numbers.map { (number) -> String in

var number = number

var output = ""

repeat {

output = digitNames[number % 10]! + output

number /= 10

} while number > 0

return output

}

//тип строк был выведен как [String]

//значения ["OneSix", "FiveEight", "FiveOneZero"]

Метод map(_:) вызывает замыкание один раз для каждого элемента массива. Вам не нужно указывать тип входного параметра замыкания, number, так как тип может быть выведен из значений массива, который применяет метод map.

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

Замыкающее выражение строит строку, названную output, каждый раз, когда оно вызывается. Оно рассчитывает последнюю цифру number, используя оператор деления с остатком ( number % 10 ) и использует затем эту получившуюся цифру, чтобы найти соответствующую строку в словаре digitNames. Это замыкание может быть использовано для создания строкового представления любого целого числа, большего чем 0.

Заметка

Вызов словаря digitNames синтаксисом сабскрипта сопровождается знаком (!), потому что сабскрипт словаря возвращает опциональное значение, так как есть такая вероятность, что такого ключа в словаре может и не быть. В примере выше мы точно знаем, что number % 10 всегда вернет существующий ключ словаря digitNames, так что восклицательный знак используется для принудительного извлечения значения типа String в возвращаемом опциональном значении сабскрипта.

Строка, полученная из словаря digitNames, добавляется в начало переменной output, путем правильного формирования строковой версии числа наоборот.(Выражение number % 10 дает нам 6 для 16, 8 для 58 и 0 для 510).

Переменная number после вычисления остатка делится на 10. Так как тип значения Int, то наше число округляется вниз, таким образом 16 превращается в 1, 58 в 5, 510 в 51.

Процесс повторяется пока number /= 10 не станет равным 0, после чего строка output возвращается замыканием и добавляется к выходному массиву функции map(_:).

Использование синтаксиса последующих замыканий в примере выше аккуратно инкапсулирует функциональность замыкания сразу после функции map(_:), которой замыкание помогает, без необходимости заворачивания всего замыкания внутрь внешних круглых скобок функции map(_:).

Захват значений

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

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

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

func makeIncrementer(forIncrement amount: Int) -> () -> Int {

var runningTotal = 0

func incrementer() -> Int {

runningTotal += amount

return runningTotal

}

return incrementer

}

Возвращаемый тип makeIncrementer Void -> Int. Это значит, что он возвращает функцию, а не простое значение. Возвращенная функция не имеет параметров и возвращает Int каждый раз как ее вызывают. Узнать как функции могут возвращать другие функции можно в главе "Функциональные типы как возвращаемые типы".

Функция makeIncrementer(forIncrement:) объявляет целочисленную переменную runningTotal, для хранения текущего значения инкрементора, которое будет возвращено. Переменная инициализируется значением 0.

Функция makeIncrementer(forIncrement:) имеет единственный параметр Int с внешним именем forIncrement и локальным именем amount. Значение аргумента передается этому параметру, определяя на сколько должно быть увеличено значение runningTotal каждый раз при вызове функции.

Функция makeIncrementer объявляет вложенную функцию incrementer, которая непосредственно и занимается увеличением значения. Эта функция просто добавляет amount к runningTotal и возвращает результат.

Если рассматривать функцию incrementer() отдельно, то она может показаться необычной:

func incrementer() -> Int {

runningTotal += amount

return runningTotal

}

Функция incrementer() не имеет ни одного параметра и она ссылается на runningTotal и amount внутри тела функции. Она делает это, захватывая существующие значения от runningTotal и amount из окружающей функции и используя их внутри. Захват ссылки дает гарантию того, что runningTotal не исчезнет при окончании вызова makeIncrementer и гарантирует, что runningTotal останется переменной в следующий раз, когда будет вызвана функция incrementer().

Заметка

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

Приведем пример makeIncrementer в действии:

let incrementByTen = makeIncrementer(forIncrement: 10)

Этот пример заставляет константу incrementByTen ссылаться на функцию инкрементора, которая добавляет 10 к значению переменной runningTotal каждый раз как вызывается. Многократный вызов функции показывает ее в действии:

incrementByTen()

// возвращает 10

incrementByTen()

// возвращает 20

incrementByTen()

// возвращает 30

Если вы создаете второй инкрементор, он будет иметь свою собственную ссылку на новую отдельную переменную runningTotal :

let incrementBySeven = makeIncrementer(forIncrement: 7)

incrementBySeven()

//возвращает значение 7

Повторный вызов первоначального инкрементора ( incrementByTen ) заставит увеличиваться его собственную переменную runningTotal и никак не повлияет на переменную, захваченную в incrementBySeven :

incrementByTen()

//возвращает 40

Заметка

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

Замыкания - ссылочный тип

В примере выше incrementBySeven и incrementByTen константы, но замыкания, на которые ссылаются эти константы имеют возможность увеличивать переменные runningTotal, которые они захватили. Это из-за того, что функции и замыкания являются ссылочными типами.

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

Это так же значит, что если вы присвоите замыкание двум разным константам или переменным, то оба они будут ссылаться на одно и то же замыкание:

let alsoIncrementByTen = incrementByTen

alsoIncrementByTen()

//возвращает 50

Сбегающие замыкания

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

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

var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {

completionHandlers.append(completionHandler)

}

Функция someFunctionWithEscapingClosure(_:) принимает и добавляет в массив замыкание, объявленный за пределами функции. Если вы не поставите маркировку @escaping, то получите ошибку компиляции.

Определение замыкания через @escaping означает, что вы должны сослаться на self явно внутри самого замыкания. Например, в коде ниже, замыкание передается функции someFunctionWithEscapingClosure(_:) в виде сбегающего замыкания, так что вам нужно ссылаться на self внутри него явно. С другой стороны, замыкание, передаваемое в someFunctionWithNonescapingClosure(_:) является не сбегающим, значит вы можете ссылаться на self неявно.

func someFunctionWithNonescapingClosure(closure: () -> Void) {

closure()

}

class SomeClass {

var x = 10

func doSomething() {

someFunctionWithEscapingClosure { self.x = 100 }

someFunctionWithNonescapingClosure { x = 200 }

}

}

let instance = SomeClass()

instance.doSomething()

print(instance.x)

// Выведет "200"

completionHandlers.first?()

print(instance.x)

// Выведет "100"

Автозамыкания (autoclosures)

Автозамыкания - замыкания, которые автоматически создаются для заключения выражения, которое было передано в качестве аргумента функции. Такие замыкания не принимают никаких аргументов при вызове и возвращают значение выражения, которое заключено внутри нее. Синтаксически вы можете опустить круглые скобки функции вокруг параметров функции, просто записав обычное выражение вместо явного замыкания.

Нет ничего необычного в вызове функций, которые принимают автозамыкания, но необычным является реализовывать такие функции. Например, функция assert(condition:message:file:line:) принимает автозамыкания на место condition и message параметров. Ее параметр condition вычисляется только в сборке дебаггера, а параметр message вычисляется, если только condition равен false.

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

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

print(customersInLine.count)

// Выведет "5"

let customerProvider = { customersInLine.remove(at: 0) }

print(customersInLine.count)

// Выведет "5"

print("Now serving \(customerProvider())!")

// Выведет "Now serving Chris!"

print(customersInLine.count)

// Выведет "4"

Даже если первый элемент массива customersInLine удаляется кодом внутри замыкания, элемент массива фактически не удаляется до тех пор пока само замыкание не будет вызвано. Если замыкание так и не вызывается, то выражение внутри него никогда не выполнится и, соответственно, элемент не будет удален из массива. Обратите внимание, что customerProvider является не String, а () -> String, то есть функция не принимает аргументов, но возвращает строку. Вы получите то же самое поведение, когда сделаете это внутри функции:

// customersInLine равен ["Alex", "Ewa", "Barry", "Daniella"]

func serve(customer customerProvider: () -> String) {

print("Now serving \(customerProvider())!")

}

serve(customer: { customersInLine.remove(at: 0) } )

// Выведет "Now serving Alex!"

Функция serve(customer:) описанная выше принимает явное замыкание, которое возвращает имя клиента. Версия функции serve(customer:) ниже выполняет ту же самую операцию, но вместо использования явного замыкания, она использует автозамыкание, поставив маркировку при помощи атрибута @autoclosure. Теперь вы можете вызывать функцию, как будто бы она принимает аргумент String вместо замыкания. Аргумент автоматически преобразуется в замыкание, потому что тип параметра customerProvider имеет атрибут @autoclosure.

// customersInLine равен ["Ewa", "Barry", "Daniella"]

func serve(customer customerProvider: @autoclosure () -> String) {

print("Now serving \(customerProvider())!")

}

serve(customer: customersInLine.remove(at: 0))

// Выведет "Now serving Ewa!"

Заметка

Слишком частое использование автозамыканий может сделать ваш код сложным для чтения. Контекст и имя функции должны обеспечивать ясность отложенности исполнения кода.

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

// customersInLine равен ["Barry", "Daniella"]

var customerProviders: [() -> String] = []

func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {

customerProviders.append(customerProvider)

}

collectCustomerProviders(customersInLine.remove(at: 0))

collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")

// Выведет "Collected 2 closures."

for customerProvider in customerProviders {

print("Now serving \(customerProvider())!")

}

// Выведет "Now serving Barry!"

// Выведет "Now serving Daniella!"

В коде выше, вместо того, чтобы вызывать переданное замыкание в качестве аргумента customer, функция collectCustomerProviders(_:) добавляет замыкание к массиву customerProviders. Массив объявлен за пределами функции, что означает, что замыкание в массиве может быть исполнено после того, как функция вернет значение. В результате значение аргумента customerProvider должен иметь “разрешение” на “побег” из зоны видимости функции.

Перечисления

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

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

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

Перечисления в Swift - типы “первого класса”. Они обладают особенностями, которые обычно поддерживаются классами, например, вычисляемые свойства, для предоставления дополнительной информации о текущем значении перечисления, методы экземпляра для дополнительной функциональности, относящейся к значениям, которые предоставляет перечисление.

Перечисления так же могут объявлять инициализаторы для предоставления начального значения элементам. Они так же могут быть расширены для наращивания своей функциональности над её начальной реализацией. Могут соответствовать протоколам для обеспечения стандартной функциональности.

Больше про эти возможности можно прочитать в разделах Свойства, Методы, Инициализация, Расширения и Протоколы.

Синтаксис перечислений

Перечисления начинаются с ключевого слова enum, после которого идет имя перечисления и полное его определение в фигурных скобках:

enum SomeEnumeration {

//здесь будет объявление перечисления

}

Ниже пример с четырьмя сторонами света:

enum CompassPoint {

case north

case south

case east

case west

}

Значения, объявленные в перечислении (north, south, east, и west), называются кейсами перечисления. Используйте ключевое слово case для включения нового кейса перечисления.

Заметка

В отличии от C и Objective-C в Swift кейсам перечисления не присваиваются целочисленные значения по умолчанию при их создании. В примере выше CompassPoint, значения членов north, south, east и west неявно не равны 0, 1, 2, 3. Вместо этого различные члены перечисления по праву полностью самостоятельны, с явно объявленным типом CompassPoint.

Множественные значения члена перечисления могут записываться в одну строку, разделяясь между собой запятой:

enum Planet {

case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune

}

Каждое объявление перечисления объявляет и новый тип. Как и остальные типы в Swift, их имена (к примеру CompassPoint и Planet) должны начинаться с заглавной буквы. Имена перечислениям лучше давать особенные, а не те, которые вы можете использовать в нескольких местах, так чтобы они читались как само собой разумеющиеся:

var directionToHead = CompassPoint.west

Тип directionToHead выведен при инициализации одного из возможных значений CompassPoint. Если directionToHead объявлена как CompassPoint, то можем использовать различные значения CompassPoint через сокращенный точечный синтаксис:

directionToHead = .east

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

Использование перечислений с инструкцией switch

Вы можете сочетать индивидуальные значения перечисления с инструкцией switch:

directionToHead = .south

switch directionToHead {

case .north:

print("Lots of planets have a north")

case .south:

print("Watch out for penguins")

case .east:

print("Where the sun rises")

case .west:

print("Where the skies are blue")

}

// Выводит "Watch out for penguins"

Вы можете прочитать этот код как:

“Рассмотрим значение directionToHead. В случае, когда directionToHead равняется .north, выводится сообщение ”Lots of planets have a north“. В кейсе, где оно равняется .south, выводится сообщение ”Watch out for penguins".

…и так далее…

Как сказано в главе “Управление потоком”, оператор switch должен быть исчерпывающим, когда рассматриваются члены перечисления. Если мы пропустим case .west, то код не скомпилируется, так как не рассматривается полный перечень членов CompassPoint. Требования к конструкции быть исчерпывающей, помогает случайно не пропустить член перечисления.

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

let somePlanet = Planet.earth

switch somePlanet {

case .earth:

print("Mostly harmless")

default:

print("Not a safe place for humans")

}

// Выведет "Mostly harmless"

Итерация по кейсам перечисления

Для некоторых перечислений можно получить коллекцию всех кейсов перечисления. Нужно лишь написать: CaseIterable после имени перечисления. Swift предоставляет коллекцию всех кейсов, как свойство allCases типа перечисления. Пример:

enum Beverage: CaseIterable {

case coffee, tea, juice

}

let numberOfChoices = Beverage.allCases.count

print("\(numberOfChoices) beverages available")

// Выведет "3 beverages available"

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

for beverage in Beverage.allCases {

print(beverage)

}

// coffee

// tea

// juice

Связанные значения

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

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

Для примера, предположим систему инвентаризации, которая должна отслеживать товар двумя различными типами штрих-кодов. Одни товары имеют коды типа 1D формата UPC-A, которые используют цифры от 0 до 9. Каждый штрих-код имеет свою “систему цифр”, где идут пять цифр “кода производителя” и пять цифр “кода продукта”. Затем идет “проверочная” цифра, которая проверяет, что код был отсканирован корректно:

Другие продукты имеют маркировку штрих-кодом 2D формата QR, который может использовать любой символ из ISO 8859–1 и может закодировать строку длиною 2953 символа:

Было бы удобно, если бы система контроля и учета товара могла бы хранить штрих-коды формата UPC-A, как кортеж из четырех целых чисел и QR код, как строку любой длины.

В Swift перечисления для определения штрих-кода продукта одного из двух типов может выглядеть следующим образом:

enum Barcode {

case upc(Int, Int, Int, Int)

case qrCode(String)

}

Читается это вот так:

“Объявление перечисления типа Barcode, которое берет два значения, одно из которых upc, со связанным значением типа (Int, Int, Int, Int) и значение qrCode со связанным значением типа String.”

Объявление не дает никакого значения типа Int или String, оно лишь определяет типы связанных значений, которые константы или переменные Barcode могут содержать, когда они равны Barcode.upc или Barcode.qrCode.

Новые штрих-коды могут быть созданы с помощью любого типа:

var productBarcode = Barcode.upc(8, 85909, 51226, 3)

В этом примере мы создаем новую переменную productBarcode и присваиваем ей значение Barcode.upc со связанным кортежем значений (8, 85909, 51226, 3).

Этому же продукту может быть присвоено другое значение кода:

productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

Здесь исходный Barcode.upc и его целочисленные значения заменены новым Barcode.qrCode и его строковым значением. Константы и переменные типа Barcode могут хранить или .upc или .qrCode (вместе со связанными значениями), но они могут хранить только один из них в любой момент времени.

Различные типы штрих-кодов могут быть проверены инструкцией switch как и раньше. В этот раз связанные значения могут быть извлечены как часть инструкции switch. Вы извлекаете каждое связанное значение как константу (с префиксом let) или как переменную (префикс var) для использования внутри тела оператора switch:

switch productBarcode {

case .upc(let numberSystem, let manufacturer, let product, let check):

print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")

case .qrCode(let productCode):

print("QR code: \(productCode).")

}

// Выводит "QR code: ABCDEFGHIJKLMNOP."

Если все связанные значения для членов перечисления извлекаются как константы или переменные, то для краткости вы можете разместить одиночное let или var перед именем члена:

switch productBarcode {

case let .upc(numberSystem, manufacturer, product, check):

print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")

case let .qrCode(productCode):

print("QR code: \(productCode).")

}

// Выведет "QR code: ABCDEFGHIJKLMNOP."

Исходные значения

В примере с Barcode в главе “Связанные значения” можно увидеть как кейсы перечисления могут объявлять значения различных типов, которые они могут хранить. Как альтернатива связанным значениям, кейсы перечисления могут иметь начальные значения (называются “исходными значениями”), которые всегда одного типа.

Вот пример перечисления, члены которого хранят исходные значения ASCII, прописанные рядом:

enum ASCIIControlCharacter: Character {

case tab = "\t"

case lineFeed = "\n"

case carriageReturn = "\r"

}

Исходные значения перечисления ASCIIControlCharacter определены как тип Character, и им присвоены распространенные контрольные символы ASCII, которые описаны в разделе “Строки и символы”.

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

Заметка

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

Неявно установленные исходные значения

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

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

Перечисление ниже представляет собой уточнение ранее указанного перечисления Planet, с целочисленными исходными значениями для представления удаленности каждой планеты от солнца:

enum Planet: Int {

case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune

}

В приведенном выше примере, Planet.mercury имеет явное исходное значение 1, Planet.venus имеет неявное исходное значение 2, и так далее.

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

Перечисление ниже представляет собой уточнение ранее указанного перечисления CompassPoint с исходными значениями строк для представления имени каждого направления:

enum CompassPoint: String {

case north, south, east, west

}

В приведенном выше примере, CompassPoint.south имеет неявное исходное значение "south" и так далее.

Для доступа к исходному значению кейса перечисления существует свойство rawValue:

let earthsOrder = Planet.earth.rawValue

// значение earthsOrder равно 3

let sunsetDirection = CompassPoint.west.rawValue

// значение sunsetDirection равно "west"

Инициализация через исходное значение

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

В этом примере Uranus инициализируется через его исходное значение 7:

let possiblePlanet = Planet(rawValue: 7)

// possiblePlanet имеет тип Planet? и его значение равно Planet.uranus

Конечно не все возможные значения Int найдут отклик в данном перечислении. Из-за этого инициализаторы исходных значений всегда возвращают опциональный член перечисления. В этом примере possiblePlanet типа Planet? или “опциональный Planet”.

Заметка

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

Если вы попытаетесь найти планету с номером позиции 11, то значение опциональной Planet, возвращенное исходным значением инициализатора, будет nil:

let positionToFind = 11

if let somePlanet = Planet(rawValue: positionToFind) {

switch somePlanet {

case .earth:

print("Mostly harmless")

default:

print("Not a safe place for humans")

}

} else {

print("There isn't a planet at position \(positionToFind)")

}

// Выведет "There isn't a planet at position 11"

Этот пример использует привязку опционалов для попытки добраться до Planet с исходным значением 11. Выражение if let somePlanet = Planet(rawValue: 11) создает опциональную Planet и устанавливает значение somePlanet опциональной Planet, если она может быть восстановлена. В этом случае невозможно добраться до планеты с позицией 11, таким образом срабатывает ветка else.

Рекурсивные перечисления

Например, ниже объявлено перечисление, которое хранит простые арифметические выражения:

enum ArithmeticExpression {

case number(Int)

indirect case addition(ArithmeticExpression, ArithmeticExpression)

indirect case multiplication(ArithmeticExpression, ArithmeticExpression)

}

Вы так же можете написать indirect прямо перед самим перечислением, что позволит обозначить то, что все члены перечисления поддерживают индиректность:

indirect enum ArithmeticExpression {

case number(Int)

case addition(ArithmeticExpression, ArithmeticExpression)

case multiplication(ArithmeticExpression, ArithmeticExpression)

}

Перечисление может хранить три вида арифметических выражений: простое число, сложение двух выражений, умножение двух выражений. Члены addition и multiplication имеют два связанных значения, которые так же являются арифметическими выражениями. Эти связанные значения делают возможным вложение выражений. Например, выражение (5 + 4) * 2 имеет цифру справа от умножения и другое выражение слева от умножения. Поскольку данные вложены, перечисление использующееся для хранения данных, также должно поддерживать вложенность-это означает, что перечисление должно быть рекурсивными. Приведенный ниже код показывает как работает рекурсивное перечисление ArithmeticExpression для (5 + 4) * 2:

let five = ArithmeticExpression.number(5)

let four = ArithmeticExpression.number(4)

let sum = ArithmeticExpression.addition(five, four)

let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

Рекурсивные функции - самый простой путь работать с данными, которые имеют рекурсивную структуру. Например, ниже приведен пример, как функция вычисляет арифметическое выражение:

func evaluate(_ expression: ArithmeticExpression) -> Int {

switch expression {

case let .number(value):

return value

case let .addition(left, right):

return evaluate(left) + evaluate(right)

case let .multiplication(left, right):

return evaluate(left) * evaluate(right)

}

}

print(evaluate(product))

// Выведет "18"

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

Структуры и классы

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

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

Заметка

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

Сравнение классов и структур

Классы и структуры в Swift имеют много общего. И в классах и в структурах можно:

Объявлять свойства для хранения значений

Объявлять методы, чтобы обеспечить функциональность

Объявлять индексы, чтобы обеспечить доступ к их значениям, через синтаксис индексов

Объявлять инициализаторы, чтобы установить их первоначальное состояние

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

Они оба могут соответствовать протоколам, для обеспечения стандартной функциональности определенного типа

Для получения дополнительной информации смотрите главы Свойства, Методы, Индексы, Инициализация, Расширения и Протоколы.

Классы имеют дополнительную возможность, которых нет у структур:

Наследование позволяет одному классу наследовать характеристики другого

Приведение типов позволяет проверить и интерпретировать тип экземпляра класса в процессе выполнения

Деинициализаторы позволяют экземпляру класса освободить любые ресурсы, которые он использовал

Подсчет ссылок допускает более чем одну ссылку на экземпляр класса. Для получения дополнительной информации смотрите Наследование, Приведение типов, Деинициализаторы и Автоматический подсчет ссылок.

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

Синтаксис объявления

Классы и структуры имеют схожий синтаксис объявления. Для объявления классов, используйте ключевое слово class, а для структур - ключевое слово struct. В обоих случаях необходимо поместить все определение полностью внутрь пары фигурных скобок:

class SomeClass {

// здесь пишется определение класса

}

struct SomeStructure {

// здесь пишется определение структуры

}

Заметка

Чего бы вы не создавали, новый класс или структуру, вы фактически создаете новый тип в Swift. Назначайте имена типов, используя UpperCamelCase(SomeClass или SomeStructure), чтобы соответствовать стандартам написания имен типов в Swift (например, String, Int и Bool). С другой стороны, всегда назначайте свойствам и методам имена в lowerCamelCase (например, frameRate и incrementCount), чтобы отличить их от имен типов.

Пример определения структуры и класса:

struct Resolution {

var width = 0

var height = 0

}

class VideoMode {

var resolution = Resolution()

var interlaced = false

var frameRate = 0.0

var name: String?

}

Пример выше объявляет новую структуру Resolution для описания разрешения монитора в пикселях. Эта структура имеет два свойства width, height. Хранимые свойства - или константы, или переменные, которые сгруппированы и сохранены в рамках класса или структуры. Этим свойствам выведен тип Int, так как мы им присвоили целочисленное значение 0.

В примере мы так же объявили и новый класс VideoMode, для описания видеорежима для отображения на видеодисплее. У класса есть четыре свойства в виде переменных. Первое - resolution, инициализировано с помощью экземпляра структуры Resolution, что выводит тип свойства как Resolution. Для остальных трех свойств новый экземпляр класса будет инициализирован с interlaced = false, frameRate = 0.0 и опциональным значением типа String с названием name. Это свойство name автоматически будет иметь значение nil или "нет значения для name", потому что это опциональный тип.

Экземпляры класса и структуры

Объявление структуры Resolution и класса VideoMode только описывают как Resolution и VideoMode будут выглядеть. Сами по себе они не описывают специфическое разрешение или видеорежим. Для того чтобы это сделать нам нужно создать экземпляр структуры или класса.

Синтаксис для образования экземпляра класса или структуры очень схож:

let someResolution = Resolution()

let someVideoMode = VideoMode()

И классы и структуры используют синтаксис инициализатора для образования новых экземпляров. Самая простая форма синтаксиса инициализатора - использование имени типа и пустые круглые скобки сразу после него Resolution(), VideoMode(). Это создает новый экземпляр класса или структуры с любыми инициализированными свойствами с их значениями по умолчанию. Классы и структуры инициализации описаны более подробно в главе Инициализация.

Доступ к свойствам

Вы можете получить доступ к свойствам экземпляра, используя точечный синтаксис. В точечном синтаксисе имя свойства пишется сразу после имени экземпляра, а между ними вписывается точка (.) без пробелов:

print("The width of someResolution is \(someResolution.width)")

// Выведет "The width of someResolution is 0"

В этом примере someResolution.width ссылается на свойство width экземпляра someResolution, у которого начальное значение равно 0.

Вы можете углубиться в подсвойства, например, свойство width свойства resolution класса VideoMode:

print("The width of someVideoMode is \(someVideoMode.resolution.width)")

// Выведет "The width of someVideoMode is 0"

Вы так же можете использовать точечный синтаксис для присваивания нового значения свойству:

someVideoMode.resolution.width = 1280

print("The width of someVideoMode is now \(someVideoMode.resolution.width)")

// Выведет "The width of someVideoMode is now 1280"

Поэлементные инициализаторы структурных типов

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

let vga = Resolution(width: 640, height: 480)

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

Структуры и перечисления - типы значения

Тип значения - это тип, значение которого копируется, когда оно присваивается константе или переменной, или когда передается функции.

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

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

Заметка

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

Рассмотрим пример, который использует структуру Resolution из предыдущего примера:

let hd = Resolution(width: 1920, height: 1080)

var cinema = hd

Этот пример объявляет константу hd и присваивает экземпляр Resolution, инициализированное двумя значениями width и height.

В свою очередь объявляем переменную cinema и присваиваем ей текущее значение hd. Так как Resolution - структура, делается копия существующего экземпляра, и эта новая копия присваивается cinema. Даже не смотря на то, что hd и cinema имеют одни и те же height, width, они являются абсолютно разными экземплярами.

Следующим шагом изменим значение свойства width у cinema, мы сделаем его чуть больше 2 тысяч, что является стандартным для цифровой кинопроекции (2048 пикселей ширины на 1080 пикселей высоты):

cinema.width = 2048

Если мы проверим свойство width у cinema, то мы увидим, что оно на самом деле изменилось на 2048:

print("cinema is now \(cinema.width) pixels wide")

// Выведет "cinema is now 2048 pixels wide"

Однако свойство width исходного hd экземпляра осталось 1920:

print("hd is still \(hd.width) pixels wide")

// Выведет "hd is still 1920 pixels wide"

Когда мы присвоили cinema текущее значение hd, то значения, которые хранились в hd были скопированы в новый экземпляр cinema. И в качестве результата мы имеем два совершенно отдельных экземпляра, которые содержат одинаковые числовые значения. Так как они являются раздельными экземплярами, то установив значение свойства width у cinema на 2048 никак не повлияет на значение width у hd, это показано ниже:

То же поведение применимо к перечислениям:

enum CompassPoint {

case north, south, east, west

mutating func turnNorth() {

self = .north

}

}

var currentDirection = CompassPoint.west

let rememberedDirection = currentDirection

currentDirection.turnNorth()

print("Текущее направление - \(currentDirection)")

// Выведет "Текущее направление - north"

Когда мы присваиваем rememberedDirection значение currentDirection, мы фактически копируем это значение. Изменяя значение currentDirection, мы не меняем копию исходного значения, хранящейся в rememberedDirection.

Классы - ссылочный тип

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

Вот пример с использованием класса VideoMode, который был объявлен выше:

let tenEighty = VideoMode()

tenEighty.resolution = hd

tenEighty.interlaced = true

tenEighty.name = "1080i"

tenEighty.frameRate = 25.0

В этом примере объявляем новую константу tenEighty и устанавливаем ссылку на новый экземпляр класса VideoMode. Значения видеорежима были присвоены копией со значениями 1920 на 1080. Мы ставим tenEighty.interlaced = true и даем имя “1080i”. Затем устанавливаем частоту кадров в секунду 25 .

Следующее, что мы сделаем, это tenEighty присвоим новой константе alsoTenEighty и изменим частоту кадров:

let alsoTenEighty = tenEighty

alsoTenEighty.frameRate = 30.0

Так как это классы ссылочного типа, то экземпляры tenEighty и alsoTenEighty ссылаются на один и тот же экземпляр VideoMode. Фактически получается, что у нас два разных имени для одного единственного экземпляра, как показано на рисунке ниже:

Если мы проверим свойство frameRate у tenEighty, то мы увидим, что новая частота кадров 30.0, которая берется у экземпляра VideoMode:

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")

// Выведет "The frameRate property of tenEighty is now 30.0"

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

Обратите внимание, что tenEighty и alsoTenEighty объявлены как константы, а не переменные. Однако вы все равно можете менять tenEighty.frameRate и alsoTenEighty.frameRate, потому что значения tenEighty и alsoTenEighty сами по себе не меняются, так как они не «содержат» значение экземпляра VideoMode, а напротив, они лишь ссылаются на него. Это свойство frameRate лежащего в основе VideoMode, которое меняется, а не значения константы ссылающейся на VideoMode.

Операторы тождественности

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

Иногда бывает полезно выяснить ссылаются ли две константы или переменные на один и тот же экземпляр класса. Для проверки этого в Swift есть два оператора тождественности:

Идентичен (===)

Не идентичен ( !== )

Можно использовать эти операторы для проверки того, ссылаются ли две константы или переменные на один и тот же экземпляр:

if tenEighty === alsoTenEighty {

print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")

}

// Выведет "tenEighty and alsoTenEighty refer to the same VideoMode instance."

Обратите внимание, что «идентичность» (в виде трех знаков равенства, или ===) не имеет в виду «равенство» (в виду двух знаков равенства, или ==). Идентичность или тождественность значит, что две константы или переменные ссылаются на один и тот же экземпляр класса. Равенство значит, что экземпляры равны или эквивалентны в значении в самом обычном понимании «равны».

Когда вы объявляете свой пользовательский класс или структуру, вы сами решаете, что означает "равенство" двух экземпляров. Процесс определения своей собственной реализации операторов "равенства" или "неравенства" описан в разделе Операторы эквивалентности.

Указатели

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

Свойства

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

Свойства хранения и вычисляемые свойства обычно связаны с экземплярами конкретного типа. Однако свойства так же могут быть связаны и с типом самим по себе. Такие свойства известны как свойства типа.

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

Свойства хранения

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

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

Пример ниже объявляет структуру FixedLengthRange, которая описывает диапазон целых чисел и длина которой не может быть изменена после установки первого значения:

struct FixedLengthRange {

var firstValue: Int

let length: Int

}

var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)

// диапазон чисел 0, 1, 2

rangeOfThreeItems.firstValue = 6

// сейчас диапазон чисел 6, 7, 8

Экземпляры FixedLengthRange имеют переменное свойство хранения firstValue и свойство хранения в виде константы length. В примере выше свойство length инициализировано, когда мы создали новый диапазон, который не может быть изменен после, так как это свойство константа.

Свойства хранения постоянных экземпляров структуры

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

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)

// теперь диапазон чисел выглядит как 0, 1, 2, 3

rangeOfFourItems.firstValue = 6

// это вызовет ошибку, даже несмотря на то, что firstValue переменная

Из-за того, что rangeOfFourItems объявлена в качестве константы (ключевое слово let), то невозможно поменять свойство firstValue, даже несмотря на то, что это свойство переменная.

Такое поведение объясняется тем, что структура является типом значений. Когда экземпляр типа значений отмечен как константа, то все его свойства так же считаются константами.

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

Ленивые свойства хранения

Ленивое свойство хранения - свойство, начальное значение которого не вычисляется до первого использования. Индикатор ленивого свойства - ключевое слово lazy.

Заметка

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

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

Пример ниже использует ленивое хранение свойства для избежания ненужной инициализации сложного класса. Этот пример объявляет два класса DataImporter и DataManager, но ни один из них мы не показали полностью:

class DataImporter {

/*

DataImporter - класс для импорта данных с внешних источников

Считаем, что классу требуется большое количество времени для инициализации

*/

var fileName = "data.txt"

// класс DataImporter функционал данных будет описан тут

}

class DataManager {

lazy var importer = DataImporter()

var data = [String]()

// класс DataManager обеспечит необходимую функциональность тут

}

let manager = DataManager()

manager.data.append("Some data")

manager.data.append("Some more data")

// экземпляр класса DataImporter для свойства importer еще не создано

Класс DataManager хранит свойство data, которое инициализируется с новым пустым массивом значений типа String. Несмотря на то, что большая часть функциональности недоступна, цель класса DataManager - это управление и обеспечение доступа к этому массиву данных типа String.

Часть функциональности класса DataManager - это импорт данных из файла. Эта функциональность обеспечивается классом DataImporter, который, как мы подразумеваем, требует уйму времени для инициализации. Это может происходить из-за того, что экземпляр класса DataImporter должен открыть файл и прочитать его содержимое в памяти, когда DataImporter уже инициализирован.

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

Так как он создан как модификатор lazy, экземпляр DataImporter для свойства importer создается только тогда, когда впервые к нему обращаются, например когда запрашивается свойство fileName:

print(manager.importer.fileName)

// экземпляр DataImporter для свойства importer только что был создан

// Выведет "data.txt"

Заметка

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

Свойства хранения и переменные экземпляра

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

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

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

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

struct Point {

var x = 0.0, y = 0.0

}

struct Size {

var width = 0.0, height = 0.0

}

struct Rect {

var origin = Point()

var size = Size()

var center: Point {

get {

let centerX = origin.x + (size.width / 2)

let centerY = origin.y + (size.height / 2)

return Point(x: centerX, y: centerY)

}

set(newCenter) {

origin.x = newCenter.x - (size.width / 2)

origin.y = newCenter.y - (size.height / 2)

}

}

}

var square = Rect(origin: Point(x: 0.0, y: 0.0),

size: Size(width: 10.0, height: 10.0))

let initialSquareCenter = square.center

square.center = Point(x: 15.0, y: 15.0)

print("square.origin is now at (\(square.origin.x), \(square.origin.y))")

// Выведет "square.origin is now at (10.0, 10.0)"

Этот пример определяет три структуры для работы с геометрическими фигурами:

Point инкапсулирует координаты (x, y).

Size инкапсулирует width, height.

Rect определяет прямоугольник по начальной точке и размеру.

Структура Rect так же обеспечивает нас вычисляемым свойством center. Текущий центр Rect может быть определен из origin и size, так что вам не нужно хранить точку центра как явное значение Point. Вместо этого Rect объявляет пользовательский геттер и сеттер для вычисляемой переменной называемой center, для того чтобы была возможность работать с center прямоугольника, как если бы она была обычным свойством хранения.

Предшествующий пример создает новую переменную square типа Rect. Переменная square инициализирована с начальной точкой (0, 0), с высотой и шириной равными 10. Этот квадрат представлен на диаграмме голубым цветом.

Свойство center переменной square доступно тогда, когда мы используем точечный синтаксис (square.center), что вызывает геттер для center, для получения текущего значения свойства. Вместо того, чтобы возвращать существующее значение, геттер считает текущее значение и возвращает новую Point, показывающую центр квадрата. Как вы можете видеть выше, геттер корректно возвращает точку центра как (5, 5).

Потом свойство center устанавливает значение (15, 15), что передвигает квадрат вверх и вправо, на новую позицию, как показано на диаграмме оранжевым квадратом. Установка свойства center вызывает сеттер для center, что обновляет значения x, y свойства origin и двигает квадрат на новую позицию.

Сокращенный вариант объявления сеттера

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

struct AlternativeRect {

var origin = Point()

var size = Size()

var center: Point {

get {

let centerX = origin.x + (size.width / 2)

let centerY = origin.y + (size.height / 2)

return Point(x: centerX, y: centerY)

}

set {

origin.x = newValue.x - (size.width / 2)

origin.y = newValue.y - (size.height / 2)

}

}

}

Вычисляемые свойства только для чтения

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

Заметка

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

Вы можете упростить объявление вычисляемых свойств только для чтения, удаляя ключевое слово get и его скобки:

struct Cuboid {

var width = 0.0, height = 0.0, depth = 0.0

var volume: Double {

return width * height * depth

}

}

let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)

print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")

// Выведет "the volume of fourByFiveByTwo is 40.0"

Этот пример объявляет новую структуру Cuboid, которая представляет 3D прямоугольную коробку с width, height, depth свойствами. Так же эта структура имеет свойство доступное только для чтения volume, которое считает и возвращает текущий объем кубойда. Никакого смысла делать volume значением установленным, так как будет не понятно какие значения width, height и depth должны быть использованы для конкретного значения объема. Тем не менее для кубойда полезно иметь вычисляемое свойство только для чтения, чтобы пользователи могли узнать текущий посчитанный объем.

Наблюдатели свойства

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

У вас есть опция определять один или оба этих наблюдателя свойства:

willSet вызывается прямо перед сохранением значения

didSet вызывается сразу после сохранения значения

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

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

Заметка

Наблюдатели willSet и didSet суперкласса вызываются, когда свойство устанавливается в инициализаторе подкласса. Они не вызываются в то время, пока класс устанавливает свои собственные свойства, до того, пока не будет вызван инициализатор суперкласса.

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

Вот пример наблюдателей willSet и didSet в действии. Пример ниже объявляет новый класс StepCounter, который следит за общим числом шагов, которые совершает человек во время прогулки. Этот класс может быть использован с входящими значениями от шагомера или другого счетчика шагов для отслеживания упражнений человека в течении всего рабочего дня.

class StepCounter {

var totalSteps: Int = 0 {

willSet(newTotalSteps) {

print("Вот-вот значение будет равно \(newTotalSteps)")

}

didSet {

if totalSteps > oldValue {

print("Добавлено \(totalSteps - oldValue) шагов")

}

}

}

}

let stepCounter = StepCounter()

stepCounter.totalSteps = 200

// Вот-вот значение будет равно 200

// Добавлено 200 шагов

stepCounter.totalSteps = 360

// Вот-вот значение будет равно 360

// Добавлено 160 шагов

stepCounter.totalSteps = 896

// Вот-вот значение будет равно 896

// Добавлено 536 шагов

Класс StepCounter объявляет свойство totalSteps типа Int. Это хранимое свойство с наблюдателями willSet, didSet.

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

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

Наблюдатель didSet вызывается после того как totalSteps обновляется. Он сравнивает новое значение totalSteps со старым. Если общее количество шагов увеличилось, то выводится сообщение о том, сколько новых шагов было сделано. Наблюдатель didSet не предоставляет имени пользовательского параметра для старого значения, но по умолчанию это имя oldValue.

Заметка

Если вы передаете свойство, имеющее наблюдателей, в функцию в качестве сквозного параметра, то наблюдатели willSet и didSet всегда вызываются. Это происходит из-за модели памяти копирования copy-in copy-out для сквозных параметров: Значение всегда записывается обратно в свойство в конце функции. Более подробное о сквозных параметрах см. Сквозные параметры.

Глобальные и локальные переменные

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

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

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

Заметка

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

Свойства типа

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

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

Свойства типа полезны при объявлении значений, которые являются универсальными для всех экземпляров конкретного типа, как например постоянное свойство, которое могут использовать все экземпляры (как например статическая константа в C), или переменное свойство, которое хранит значение и которое является глобальным для всех экземпляров данного типа (как статическая переменная в C).

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

Заметка

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

Хранимые свойства типа отложено инициализируются при первом обращении к ним. Они гарантировано инициализируются только один раз, даже если они доступны сразу для нескольких потоков. Эти свойства не нуждаются в маркировке lazy.

Синтаксис свойства типа

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

Чтобы объявить свойства типа, используйте ключевое слово static . Для вычисляемых свойств самого класса, вы должны использовать ключевое слово class, чтобы разрешать подклассам переопределение инструкций суперкласса. Пример ниже показывает синтаксис для хранимых и вычисляемых свойств типа:

struct SomeStructure {

static var storedTypeProperty = "Some value."

static var computedTypeProperty: Int {

return 1

}

}

enum SomeEnumeration {

static var storedTypeProperty = "Some value."

static var computedTypeProperty: Int {

return 6

}

}

class SomeClass {

static var storedTypeProperty = "Some value."

static var computedTypeProperty: Int {

return 27

}

class var overrideableComputedTypeProperty: Int {

return 107

}

}

Заметка

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

Запросы и установка свойств типа

Обращение к свойству типа и присваивание ему значения происходит с использованием точечного синтаксиса. Однако запрос и присваивание значения происходит в свойстве типа, а не в экземпляре того типа. К примеру:

print(SomeStructure.storedTypeProperty)

// Выведет "Some value."

SomeStructure.storedTypeProperty = "Another value."

print(SomeStructure.storedTypeProperty)

// Выведет "Another value."

print(SomeEnumeration.computedTypeProperty)

// Выведет "6"

print(SomeClass.computedTypeProperty)

// Выведет "27"

Примеры, которые будут позже, используют два хранимых свойства типа как часть структуры, которая моделирует индикатор уровня громкости для звуковых каналов. Каждый канал имеет целочисленный уровень звука между 0 и 10 включительно.

На рисунке ниже показано, как два из этих звуковых канала могут быть объединены для моделирования стерео индикатора уровня звука. Когда уровень звука канала 0, ни один из огней для этого канала не горят, но когда уровень звука 10, все огни на этом канале горят. На этом рисунке, левый канал имеет текущий уровень 9, а правый канал в настоящее время имеет уровень 7:

Аудиоканалы, описанные выше, представлены экземплярами структуры AudioChannel:

struct AudioChannel {

static let thresholdLevel = 10

static var maxInputLevelForAllChannels = 0

var currentLevel: Int = 0 {

didSet {

if currentLevel > AudioChannel.thresholdLevel {

// ограничиваем уровень звука максимально допустимым уровнем

currentLevel = AudioChannel.thresholdLevel

}

if currentLevel > AudioChannel.maxInputLevelForAllChannels {

// храним значение в качестве максимального уровня

AudioChannel.maxInputLevelForAllChannels = currentLevel

}

}

}

}

Структура AudioChannel объявляет два хранящихся свойства для поддержания функциональности. Первое - thresholdLevel определяет максимальное значение порога, которое звуковой уровень может воспроизвести. Это константное значение равное 10 для всех экземпляров AudioChannel. Если звуковой сигнал идет со значением выше чем 10, то он будет ограничен до порогового значения (как описано ниже).

Второе свойство типа - хранящееся свойство-переменная названная maxInputLevelForAllChannels. Оно отслеживает максимальное входное значение, которое было получено любым экземпляром AudioChannel. Исходное значение равно 0.

Структура AudioChannel так же определяет хранимое свойство currentLevel экземпляра, которое определяет текущий уровень аудиоканала в диапазоне от 0 до 10.

У свойства currentLevel есть обозреватель didSet для проверки значения currentLevel, всякий раз как оно присваивается. Этот наблюдатель выполняет две задачи:

Если новое значение currentLevel больше, чем допущено thresholdLevel, наблюдатель свойства ограничивает currentLevel до уровня thresholdLevel.

Если новое значение currentLevel (после возможных ограничений) больше, чем какое-либо другое, полученное экземпляром AudioChannel ранее, то наблюдатель свойства сохраняет значение нового currentLevel в статическое свойство maxInputLevelForAllChannels.

Заметка

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

Вы можете использовать структуру AudioChannel для создания двух новых аудиоканалов, названных leftChannel, rightChannel, для отображения уровней звука стерео системы:

var leftChannel = AudioChannel()

var rightChannel = AudioChannel()

Если вы установите значение currentLevel для leftChannel на 7, то вы увидите, что значение свойства maxInputLevelForAllChannels обновится и станет равным 7:

leftChannel.currentLevel = 7

print(leftChannel.currentLevel)

// Выведет "7"

print(AudioChannel.maxInputLevelForAllChannels)

// Выведет "7"

Если вы попробуете установить значение правого канала currentLevel равным 11, то вы можете увидеть, что значение свойства правого канала currentLevel будет ограничено максимальным значением 10, и значение свойства типа maxInputLevelForAllChannels обновится и станет равным 10:

rightChannel.currentLevel = 11

print(rightChannel.currentLevel)

// Выведет "10"

print(AudioChannel.maxInputLevelForAllChannels)

// Выведет "10"

Методы

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

Дело в том, что структуры и перечисления могут определить методы в Swift, что является главным отличием от C или Objective-C. В Objective-C классы единственный тип, который может определять методы. В Swift вы можете выбирать, стоит ли вам определять класс, структуру или перечисление, и вы все равно, при любом раскладе, получаете возможность определения методов типа, который вы создадите.

Методы экземпляра

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

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

Ниже пример, который определяет простой класс Counter, который может быть использован для счета количества повторений действия:

class Counter {

var count = 0

func increment() {

count += 1

}

func increment(by amount: Int) {

count += amount

}

func reset() {

count = 0

}

}

Класс Counter() определяет три метода экземпляра:

increment увеличивает значение счетчика на 1

increment(by: Int) увеличивает значение счетчика на определенное значение amount.

reset() сбрасывает значение счетчика на 0.

Класс Counter так же определяет переменное свойство count, для отслеживания значения счетчика.

Вы можете вызвать методы экземпляра с тем же точечным синтаксисом:

let counter = Counter()

// начальное значение counter равно 0

counter.increment()

// теперь значение counter равно 1

counter.increment(by: 5)

// теперь значение counter равно 6

counter.reset()

// теперь значение counter равно 0

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

Свойство self

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

Метод increment может быть вызван так:

func increment() {

self.count += 1

}

На практике вам не нужно писать self очень часто. Если вы не пишите self, то Swift полагает, что вы ссылаетесь на свойство или метод текущего экземпляра каждый раз, когда вы используете известное имя свойства или метода внутри метода. Это хорошо видно при использовании свойства count (а не self.count) внутри трех методов Counter.

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

Здесь self разграничивает параметр метода x и свойство экземпляра, которое тоже x:

struct Point {

var x = 0.0, y = 0.0

func isToTheRightOf(x: Double) -> Bool {

return self.x > x

}

}

let somePoint = Point(x: 4.0, y: 5.0)

if somePoint.isToTheRightOf(x: 1.0) {

print("Эта точка находится справа от линии, где x == 1.0")

}

// Выведет "Эта точка находится справа от линии, где x == 1.0"

Без префикса self, Swift будет думать, что в обоих случаях x будет как раз параметром метода, который так же называется x.

Изменение типов значений методами экземпляра

Структуры и перечисления являются типами значений. По умолчанию, свойства типов значений не могут быть изменены изнутри методов экземпляра.

Однако, если вам нужно изменить свойства вашей структуры или перечисления изнутри конкретного метода, то вы можете выбрать поведение как изменяющееся для этого метода. После этого метод может изменить свои свойства изнутри метода, и все изменения будут сохранены в исходную структуру, когда выполнение метода закончится. Метод так же может присвоить совершенно новый экземпляр для свойства self, и этот новый экземпляр заменит существующий, после того как выполнение метода закончится.

Вы можете все это осуществить, если поставите ключевое слово mutating перед словом func для определения метода:

struct Point {

var x = 0.0, y = 0.0

mutating func moveBy(x deltaX: Double, y deltaY: Double) {

x += deltaX

y += deltaY

}

}

var somePoint = Point(x: 1.0, y: 1.0)

somePoint.moveBy(x: 2.0, y: 3.0)

print("Сейчас эта точка на (\(somePoint.x), \(somePoint.y))")

// Выведет "Сейчас эта точка на (3.0, 4.0)"

Структура Point определяет метод moveBy(x:y:), который передвигает точку типа Point на определенное количество значений. Вместо того, чтобы вернуть новую точку, этот метод фактически изменяет координаты точки, которая его вызвала. Ключевое слово mutating добавлено к определению метода, для того, чтобы изменить значения свойств.

Обратите внимание, что вы не можете вызвать изменяющийся (mutating) метод для константных типов структуры, потому как ее свойства не могут быть изменены, даже если свойства являются переменными, что описано в главе Свойства хранения постоянных экземпляров структуры.

let fixedPoint = Point(x: 3.0, y: 3.0)

fixedPoint.moveBy(x: 2.0, y: 3.0)

// это вызовет сообщение об ошибке

Присваивание значения для self внутри изменяющегося метода

Изменяющиеся методы могут присваивать полностью новый экземпляр неявному свойству self. Пример Point, приведенный выше, мог бы быть записан в такой форме:

struct Point {

var x = 0.0, y = 0.0

mutating func moveBy(x deltaX: Double, y deltaY: Double) {

self = Point(x: x + deltaX, y: y + deltaY)

}

}

Такая версия изменяющегося метода moveBy(x:y:) создает абсолютно новую структуру, чьим значениям x, y присвоены значения конечной точки. Конечный результат вызова этой альтернативной версии метода будет абсолютно таким же как и в ранней версии.

Изменяющиеся методы для перечислений могут установить отдельный член перечисления как неявный параметр self:

enum TriStateSwitch {

case off, low, high

mutating func next() {

switch self {

case .off:

self = .low

case .low:

self = .high

case .high:

self = .off

}

}

}

var ovenLight = TriStateSwitch.low

ovenLight.next()

// ovenLight равен .high

ovenLight.next()

// ovenLight равен .off

В этом примере мы рассматриваем перечисление с тремя положениями переключателя. Переключатель проходит последовательно три положения (off, low, high), каждый раз меняя положение, как вызывается метод next().

Методы типа

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

Заметка

В Objective-C определять методы типов можно только для классов. В Swift вы можете создавать методы типа не только для классов, но и для структур и перечислений. Метод каждого типа ограничен самим типом, который он поддерживает.

Такие методы так же используют точечный синтаксис, как и методы экземпляра. Однако эти методы вы вызываете самим типом, а не экземпляром этого типа. Вот как вы можете вызвать метод самим классом SomeClass:

class SomeClass {

class func someTypeMethod(){

//здесь идет реализация метода

}

}

SomeClass.someTypeMethod()

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

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

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

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

struct LevelTracker {

static var highestUnlockedLevel = 1

var currentLevel = 1

static func unlock(_ level: Int) {

if level > highestUnlockedLevel { highestUnlockedLevel = level }

}

static func isUnlocked(_ level: Int) -> Bool {

return level <= highestUnlockedLevel

}

@discardableResult mutating func advance(to level: Int) -> Bool {

if LevelTracker.isUnlocked(level) {

currentLevel = level

return true

} else {

return false

}

}

}

Структура LevelTracker следит за самым последним уровнем, который разблокировал игрок. Это значение лежит в свойстве типа highestUnlockedLevel.

LevelTracker так же определяет две функции для работы со свойством highestUnlockedLevel. Первая функция типа unlock(_:), которая обновляет значение highestUnlockedLevel, каждый раз когда открывается новый уровень. Вторая функция типа isUnlocked(_:), которая возвращает true, если конкретный уровень уже разблокирован. (Обратите внимание, что методы типа могут получить доступ к highestUnlockedLevel без написания LevelTracker.highestUnlockedLevel.)

В дополнение к его свойствам типа и методам типа, структура LevelTracker так же отслеживает и текущий прогресс игрока в игре. Она использует свойство экземпляра currentLevel для отслеживания уровня, на котором игрок играет.

Для помощи в управлении свойством currentLevel, структура LevelTracker определяет метод экземпляра advance(to:). До того как обновить currentLevel, этот метод проверяет доступен ли запрашиваемый новый уровень. Метод advance(to:) возвращает логическое значение, указывающее, удалось ли ему поставить currentLevel. Не обязательно должно быть ошибкой игнорирование результата работы функции advance(to:), поэтому этот метод имеет марикровку @discardableResult. Об атрибутах подробнее можно прочитать в разделе Атрибуты.

Структура LevelTracker используется классом Player, который описан ниже, для отслеживания и обновления прогресса конкретного игрока:

class Player {

var tracker = LevelTracker()

let playerName: String

func complete(level: Int) {

LevelTracker.unlock(level + 1)

tracker.advance(to: level + 1)

}

init(name: String) {

playerName = name

}

}

Класс Player создает новый экземпляр LevelTracker для отслеживания прогресса игрока. Так же он определяет и использует метод complete(level:), который вызывается каждый раз, как игрок заканчивает уровень. Этот метод открывает следующий уровень для всех игроков. (Логическое значение advance(to:) игнорируется, так как уровень открывается функцией LevelTracker.unlock(_:) на предыдущей строке.)

Вы можете создать экземпляр класса Player для нового игрока и увидеть, что будет, когда игрок закончит первый уровень:

var player = Player(name: "Argyrios")

player.complete(level: 1)

print("Самый последний доступный уровень сейчас равен \(LevelTracker.highestUnlockedLevel)")

//Выведет "Самый последний доступный уровень сейчас равен 2"

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

player = Player(name: "Beto")

if player.tracker.advance(to: 6) {

print("Игрок на уровне 6")

} else {

print("Уровень 6 еще не разблокирован")

}

// Выведет "Уровень 6 еще не разблокирован"

Сабскрипты

Классы, структуры и перечисления могут определять сабскрипты, которые являются сокращенным вариантом доступа к члену коллекции, списка или последовательности. Вы можете использовать сабскрипт для получения или установки нового значения элемента без разделения этих двух методов (получения значения и установкой нового). К примеру, вы можете воспользоваться сабскриптом в экземпляре массива для получения значения элемента someArray[index] или в экземпляре словаря someDictionary[key].

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

Синтаксис сабскрипта

Сабскрипты позволяют вам запрашивать экземпляры определенного типа, написав одно или несколько значений в квадратных скобках после имени экземпляра. Синтаксис сабскрипта аналогичный синтаксису метода экземпляра и вычисляемому свойству. Вы пишете определения сабскрипта с помощью ключевого слова subscript и указываете один или более входных параметров и возвращаемый тип, точно так же как и в методах экземпляра. В отличии от методов экземпляра, сабскрипты могут быть read-write или read-only. Такое поведение сообщается геттером и сеттером в точности так же как и в вычисляемых свойствах:

subscript(index: Int) -> Int {

get {

//возвращает надлежащее значение скрипта

}

set(newValue) {

//проводит надлежащие установки

}

}

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

Как и в случае с нередактируемыми (read-only) вычисляемыми свойствами, вы можете опустить слово get для нередактируемых (read-only) сабскриптов:

subscript(index: Int) -> Int {

//возвращает надлежащее значение скрипта

}

Пример определения нередактируемого сабскрипта, который определяет структуру TimesTable показать таблицу умножения на n:

struct TimesTable{

let multiplier: Int

subscript(index: Int) -> Int {

return multiplier * index

}

}

let threeTimesTable = TimesTable(multiplier: 3)

print("шесть умножить на три будет \(threeTimesTable[6])")

// Выведет "шесть умножить на три будет 18"

В этом примере новый экземпляр TimesTable создан для отображения таблицы умножения на три. Это определяется переданным ему значением 3 в инициализатор структуры как значение для параметра экземпляра multiplier.

Вы можете запросить экземпляр threeTimesTable с помощью сабскрипта, как уже сделано в примере выше threeTimesTable[6]. Мы запрашиваем шестую запись в таблице умножения на три, которая возвращает значение 18 или 6 умноженное на 3.

Заметка

Таблица умножения на n основана на фиксированном математическом правиле. Присваивание нового значения threeTimesTable[someIndex] не подходит для нашего варианта, значит сабскрипт TimesTable определен как неизменяемый сабскрипт.

Использование сабскрипта

Точное значение “сабскрипта” зависит от контекста, в котором он применяется. Обычно сабскрипты используются в качестве сокращенного способа обращения к элементу коллекции, списка или последовательности. Вы свободны применять сабскрипты в необходимой форме для вашего класса или для функциональности структуры.

К примеру, словарь в языке Swift использует сабскрипт для присваивания или получения значения, которое хранится в экземпляре Dictionary. Вы можете задать значение в словаре, и используя ключ типа ключа словаря в квадратных скобках, присваивая значение типа словаря через сабскрипт:

var numberOfLegs = ["паук": 8, "муравей": 6, "кошка": 4]

numberOfLegs["птичка"] = 2

В примере выше мы объявляем переменную numberOfLegs и инициализируем ее с помощью литерала словаря, который содержит три пары ключ-значение. Тип словаря numberOfLegs выводится как [String: Int]. После того как словарь создан, в этом примере используется сабскриптное присваивание для добавления ключа типа String “птичка“, значения типа Int 2.

Для более подробного описания работы со словарями обратитесь к разделу Доступ и изменение словаря.

Заметка

Тип Dictionary в Swift осуществляет ключ-значение индексирование, как сабскрипт, который получает опциональное значение. Для словаря numberOfLegs, сабскрипт ключ-значение берет и возвращает значение типа Int? или “опциональный Int”. Тип Dictionary использует опциональный тип сабскрипта, чтобы смоделировать факт того, что не каждый ключ может иметь значение, и для того, чтобы была возможность удаления значения для ключа, присваивая ему nil.

Опции сабскрипта

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

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

Сейчас в большинстве случаев сабскрипт принимает один единственный параметр, вы так же можете определить сабскрипт с несколькими параметрами, если этот вариант подходит для вашего типа. Следующий пример определяет структуру Matrix, которая представляет собой двухмерную матрицу значений типа Double. Сабскрипт структуры Matrix принимает два целочисленных параметра.

struct Matrix {

let rows: Int, columns: Int

var grid: [Double]

init(rows: Int, columns: Int) {

self.rows = rows

self.columns = columns

grid = Array(repeating: 0.0, count: rows * columns)

}

func indexIsValid(row: Int, column: Int) -> Bool {

return row >= 0 && row < rows && column >= 0 && column < columns

}

subscript(row: Int, column: Int) -> Double {

get {

assert(indexIsValid(row: row, column: column), "Index out of range")

return grid[(row * columns) + column]

}

set {

assert(indexIsValid(row: row, column: column), "Index out of range")

grid[(row * columns) + column] = newValue

}

}

}

Matrix предоставляет инициализатор, который принимает два параметра rows и columns, и создает массив типа Double, который имеет размер rows * columns. Каждой позиции в матрице дается начальное значение 0.0. Чтобы этого достичь, размер массива и начальное значение клетки равное 0.0 передаются в инициализатор массива, который создает и инициализирует новый массив необходимого размера. Это описано подробнее в главе Создание массива с дефолтным значением.

Вы можете создать новый экземпляр типа Matrix, передав количество рядов и столбцов в его инициализатор:

var matrix = Matrix(rows: 2, columns: 2)

Этот пример создает новый экземпляр Matrix, который имеет всего два ряда и два столбца. Массив grid для экземпляра Matrix фактически упрощенный вариант версии матрицы, который читается с левой верхней части в правую нижнюю часть:

Значения в матрице могут быть установлены через передачу значений ряда и столбца в сабскрипте, разделенных между собой запятой:

matrix[0, 1] = 1.5

matrix[1, 0] = 3.2

Эти два выражения в сеттере сабскрипта устанавливают значения 1.5 для верхней правой позиции (где row равен 0, column равен 1), и значение 3.2 для нижней левой позиции (где row равен 1, а column равен 0 ):

Геттер и сеттер сабскрипта Matrix оба содержат утверждения для проверки валидности значений row и column. Для помощи утверждениям, Matrix имеет удобный метод под названием indexIsValid(row:column:), который проверяет наличие запрашиваемых row, column в существующей матрице:

func indexIsValid(row: Int, column: Int) -> Bool {

return row >= 0 && row < rows && column >= 0 && column < columns

}

Утверждения срабатывают, если вы пытаетесь получить доступ к сабскрипту, который находится за пределами матрицы:

let someValue = matrix[2, 2]

//это вызывает утверждение, потому что [2, 2] находится за пределами матрицы

Наследование

Класс может наследовать методы, свойства и другие характеристики другого класса. Когда один класс наследует у другого класса, то наследующий класс называется подклассом, класс у которого наследуют - суперклассом. Наследование - фундаментальное поведение, которое отделяет классы от других типов Swift.

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

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

Определение базового класса

Любой класс, который ничего не наследует из другого класса, называется базовым классом.

Заметка

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

Пример ниже определяет класс Vehicle. Этот базовый класс определяет хранимое свойство currentSpeed, с начальным значением 0.0 (выведенный тип Double). Значение свойства currentSpeed используется вычисляемым нередактируемым свойством description типа String, для создания описания транспортного средства (экземпляра Vehicle).

Так же класс Vehicle определяет метод makeNoise. Этот метод фактически ничего не делает для базового экземпляра класса Vehicle, но будет настраиваться подклассом класса Vehicle чуть позже:

class Vehicle {

var currentSpeed = 0.0

var description: String {

return "движется на скорости \(currentSpeed) миль в час"

}

func makeNoise() {

//ничего не делаем, так как не каждый транспорт шумит

}

}

Вы создаете новый экземпляр класса Vehicle при помощи синтаксиса инициализатора, который написан как TypeName, за которым идут пустые круглые скобки:

let someVehicle = Vehicle()

Создав новый экземпляр класса Vehicle, вы можете получить доступ к его свойству description, для вывода на экран описания текущей скорости транспорта:

print("Транспорт: \(someVehicle.description)")

//Транспорт: движется на скорости 0.0 миль в час

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

Наследование подклассом

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

Для индикации того, что подкласс имеет суперкласс, просто напишите имя подкласса, затем имя суперкласса и разделите их двоеточием:

class SomeSubclass: SomeSuperclass {

// определение подкласса проводится тут

}

Приведенный пример определяет подкласс Bicycle с суперклассом Vehicle:

class Bicycle: Vehicle {

var hasBasket = false

}

Новый класс Bicycle автоматически собирает все характеристики Vehicle, например, такие свойства как currentSpeed и description и метод makeNoise().

В дополнение к характеристикам, которые он наследует, класс Bicycle определяет свое новое хранимое свойство hasBasket, со значением по умолчанию false (тип свойства выведен как Bool).

По умолчанию, любой новый экземпляр Bicycle, который вы создадите не будет иметь корзину (hasBasket = false). Вы можете установить hasBasket на значение true для конкретного экземпляра Bicycle, после того как он создан:

let bicycle = Bicycle()

bicycle.hasBasket = true

Вы так же можете изменить унаследованное свойство currentSpeed экземпляра Bicycle и запросить его свойство description:

bicycle.currentSpeed = 15.0

print("Велосипед: \(bicycle.description)")

//Велосипед: движется на скорости 15.0 миль в час

Подклассы сами могут создавать подклассы. В следующем примере класс Bicycle создает подкласс для двухместного велосипеда известного как “тандем”:

class Tandem: Bicycle {

var currentNumberOfPassengers = 0

}

Класс Tandem наследует все свойства и методы Bicycle, который в свою очередь наследует все свойства и методы от Vehicle. Подкласс Tandem так же добавляет новое хранимое свойство currentNumberOfPassengers, которое по умолчанию равно 0.

Если вы создадите экземпляр Tandem, то вы можете работать с любой из его новых и наследованных свойств и запрос свойства только для чтения description он наследует от Vehicle:

let tandem = Tandem()

tandem.hasBasket = true

tandem.currentNumberOfPassengers = 2

tandem.currentSpeed = 22.0

print("Тандем: \(tandem.description)")

// Тандем: движется на скорости 22.0 миль в час

Переопределение

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

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

Ключевое слово override так же подсказывает компилятору Swift проверить, что вы переопределяете суперкласс класса (или один из его параметров), который содержит то определение, которое вы хотите переопределить. Эта проверка проверяет, что ваше переопределение определения корректно.

Доступ к методам, свойствам, индексам суперкласса

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

Там, где это уместно, вы можете получить доступ к методу, свойству, индексу версии суперкласса, если будете использовать префикс super:

Переопределенный метод someMethod может вызвать версию суперкласса метода someMethod, написав super.someMethod() внутри переопределения реализации метода.

Переопределённое свойство someProperty может получить доступ к свойству версии суперкласса someProperty как super.someProperty внутри переопределения реализации геттера или сеттера.

Переопределенный индекс для someIndex может получить доступ к версии суперкласса того же индекса как super[someIndex] изнутри переопределения реализации индекса.

Переопределение методов

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

Следующий пример определяет новый подкласс Train класса Vehicle, который переопределяет метод makeNoise(), который Train наследует от Vehicle:

class Train: Vehicle {

override func makeNoise() {

print("Чу-чу")

}

}

Если вы создаете новый экземпляр класса Train и вызовите его метод makeNoise(), вы увидите, что версия метода подкласса Train вызывается вот так:

let train = Train()

train.makeNoise()

// Выведет "Чу-чу"

Переопределение свойств

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

Переопределения геттеров и сеттеров свойства

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

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

Заметка

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

Следующий пример определяет класс Car, который является подклассом Vehicle. Класс Car предоставляет новое свойство хранения gear, имеющее значение по умолчанию равное 1. Класс Car так же переопределяет свойство description, которое он унаследовал от Vehicle, для предоставления собственного описания, которое включает в себя текущую передачу:

class Car: Vehicle {

var gear = 1

override var description: String {

return super.description + " на передаче \(gear)"

}

}

Переопределение свойства description начинается с super.description, который возвращает свойство description класса Vehicle. Версия класса Car свойства description добавляет дополнительный текст в конец описания текущего свойства description.

Если вы создадите экземпляр класса Car и зададите свойства gear, currentSpeed, то вы увидите что его свойство description возвращает новое описание класса Car:

let car = Car()

car.currentSpeed = 25.0

car.gear = 3

print("Машина: \(car.description)")

// Выведет "Машина: движется на скорости 25.0 миль в час на передаче 3"

Переопределение наблюдателей свойства

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

Заметка

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

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

В следующем примере определим новый класс AutomaticCar, который является подклассом Car. Класс AutomaticCar представляет машину с автоматической коробкой передач, которая автоматически переключает передачи в зависимости от текущей скорости:

class AutomaticCar: Car {

override var currentSpeed: Double {

didSet {

gear = Int(currentSpeed / 10.0) + 1

}

}

}

Куда бы вы не поставили свойство currentSpeed экземпляра класса AutomaticCar, наблюдатель didSet свойства устанавливает свойство экземпляра gear в подходящее значение передачи в зависимости от скорости. Если быть точным, то наблюдатель свойства выбирает передачу как значение равное currentSpeed поделенная на 10 и округленная вниз и выбираем ближайшее целое число + 1. Если скорость равно 10.0, то передача равна 1, если скорость 35.0, то передача 4:

let automatic = AutomaticCar ()

automatic.currentSpeed = 35.0

print("Машина с автоматом: \(automatic.description)")

//Выведет "Машина с автоматом: движется на скорости 35.0 миль в час на передаче 4"

Предотвращение переопределений

Вы можете предотвратить переопределение метода, свойства или индекса, обозначив его как конечный. Сделать это можно написав ключевое слово final перед ключевым словом метода, свойства или индекса (final var, final func, final class func, и final subscript ).

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

Вы можете отметить целый класс как конечный или финальный, написав слово final перед ключевым словом class (final class). Любая попытка изменить класс так же приведет к ошибке компиляции.

Инициализация

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

Вы реализуете эту инициализацию, определяя инициализаторы, которые схожи со специальными методами, которые вызываются для создания экземпляра определенного типа. В отличии от инициализаторов в Objective-C, инициализаторы в Swift не возвращают значения. Основная роль инициализаторов - убедиться в том, что новый экземпляр типа правильно инициализирован до того, как будет использован в первый раз.

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

Установка начальных значений для свойств хранения

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

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

Заметка

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

Инициализаторы

Инициализаторы вызываются для создания нового экземпляра конкретного типа. В самой простой своей форме инициализатор работает как метод экземпляра без параметров, написанный с помощью ключевого слова init:

init() {

// инициализация проводится тут

}

Пример ниже определяет новую структуру Fahrenheit для хранения температур, представленных в Фаренгейтах. Структура Fahrenheit имеет всего одно свойство, temperature типа Double:

struct Fahrenheit {

var temperature: Double

init() {

temperature = 32.0

}

}

var f = Fahrenheit()

print("Значение температуры по умолчанию \(f.temperature)° по Фаренгейту")

// Выведет "Значение температуры по умолчанию 32.0° по Фаренгейту"

Структура определяет один инициализатор, init, без параметров, который инициализирует хранимую температуру равную 32.0 (температура замерзания воды по Фаренгейту).

Дефолтные значения свойств

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

Заметка

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

Вы можете написать структуру Fahrenheit в более простой форме, указав значение по умолчанию для свойства temperature, в месте его объявления:

struct Fahrenheit {

var temperature = 32.0

}

Настройка инициализации

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

Параметры инициализации

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

Следующий пример определяет структуру Celsius, которая хранит температуру в Цельсиях. Структура Celsius реализует два пользовательских инициализатора init(fromFahrenheit:) и init(fromKelvin:), которые инициализируют новый экземпляр структуры со значением другой температурной шкалы:

struct Celsius {

var temperatureInCelsius: Double

init(fromFahrenheit fahrenheit: Double) {

temperatureInCelsius = (fahrenheit - 32.0) / 1.8

}

init(fromKelvin kelvin: Double) {

temperatureInCelsius = kelvin - 273.15

}

}

let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)

// boilingPointOfWater.temperatureInCelsius is 100.0

let freezingPointOfWater = Celsius(fromKelvin: 273.15)

// freezingPointOfWater.temperatureInCelsius is 0.0

Первый инициализатор имеет один параметр с внешним именем fromFahrinheit и с локальным именем fahrenheit. Второй инициализатор имеет один параметр с внешним именем fromKelvin и локальным именем kelvin. Оба инициализатора конвертируют их единственный аргумент в значение по Цельсию и сохраняют это значение в свойство temperatureInCelsius.

Локальные и внешние имена параметров

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

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

Следующий пример определяет структуру Color с тремя постоянными свойствами red, green, blue. Эти свойства имеют значения от 0.0 до 1.0, для индикации количества соответствующего цвета.

Color имеет инициализатор с тремя параметрами red, green, blue типа Double(компоненты цвета красного, зеленого, синего). Так же Color имеет второй инициализатор с одним параметром white, который нужен для предоставления значения для всех трех компонентов цвета.

struct Color {

let red, green, blue: Double

init(red: Double, green: Double, blue: Double) {

self.red = red

self.green = green

self.blue = blue

}

init(white: Double) {

red = white

green = white

blue = white

}

}

Оба инициализатора могут быть использованы для создания нового экземпляра Color, передав значения в каждый параметр инициализатора:

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)

let halfGray = Color(white: 0.5)

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

let veryGreen = Color(0.0, 1.0, 0.0)

// этот код вызовет ошибку компиляции, так как здесь нет внешних имен

Параметры инициализатора без внешних имен

Если вы не хотите использовать внешние имена для параметров инициализации, напишите подчеркивание (_) вместо явного указания внешнего имени для этого параметра, чтобы переопределить поведение по умолчанию.

Вот расширенный вариант для Celsius, который мы рассматривали ранее, с дополнительным инициализатором, для создания нового экземпляра Celsius с типом значения температуры Double только уже по шкале Цельсия:

struct Celsius {

var temperatureInCelsius: Double

init(fromFahrenheit fahrenheit: Double) {

temperatureInCelsius = (fahrenheit - 32.0) / 1.8

}

init(fromKelvin kelvin: Double) {

temperatureInCelsius = kelvin - 273.15

}

init(_ celsius: Double) {

temperatureInCelsius = celsius

}

}

let bodyTemperature = Celsius(37.0)

// bodyTemperature.temperatureInCelsius is 37.0

Инициализатор вызывает Celsius(37.0), что понятно и без внешнего имени параметра. Поэтому целесообразно написать init(_ celsius: Double), для того, чтобы предоставить безымянное значение типа Double.

Опциональные типы свойств

Если ваш пользовательский тип имеет свойство, которое логически имеет “отсутствие значения”, возможно потому, что его значение не может быть установлено во время инициализации или потому, что ему разрешается иметь “отсутствие значения” в какой-либо точке кода, то такое свойство нужно объявить с опциональным типом. Свойства опционального типа автоматически инициализируются со значением nil, указывая на то, что значение стремится иметь значение “пока что отсутствие значение” на этапе инициализации.

Следующий код определяет класс SurveyQuestion с опциональным типом String свойства response:

class SurveyQuestion {

var text: String

var response: String?

init(text: String) {

self.text = text

}

func ask() {

print(text)

}

}

let cheeseQuestion = SurveyQuestion(text: "Нравится ли вам сыр?")

cheeseQuestion.ask()

// Выведет "Нравится ли вам сыр?"

cheeseQuestion.response = "Да, я люблю сыр"

Ответ на вопрос не может быть известен до того, пока он не задан, так что свойство response должно быть типа String? (опциональный String). Ему автоматически присваивается значение nil при инициализации SurveyQuestion, значащее, что “значения пока нет”.

Присваивание значений постоянным свойствам во время инициализации

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

Заметка

В экземплярах класса постоянное свойство может быть изменено только во время инициализации класса, в котором оно представлено. Оно не может быть изменено подклассом.

Вы можете пересмотреть пример SurveyQuestion и использовать вместо переменного свойства text постоянное свойство text, для индикации того, что это свойство не меняется после создания SurveyQuestion. Даже если свойство является постоянным, оно все еще может быть установлено в инициализаторе класса:

class SurveyQuestion {

let text: String

var response: String?

init(text: String) {

self.text = text

}

func ask() {

print(text)

}

}

let beetsQuestion = SurveyQuestion(text: "Что насчет свеклы?")

beetsQuestion.ask()

// Выведет "Что насчет свеклы?"

beetsQuestion.response = "Я люблю свеклу, но не в сыром виде!"

Дефолтные инициализаторы

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

Этот пример определяет класс ShoppingListItem, который включает в себя имя, количество и состояние сделки на предмет из листа покупок:

class ShoppingListItem {

var name: String?

var quantity = 1

var purchased = false

}

var item = ShoppingListItem()

Так как все свойства класса ShoppingListItem имеют значения по умолчанию и так как этот класс не имеет суперкласса, то ShoppingListItem автоматически получает реализацию дефолтного инициализатора, который создает новый экземпляр со всеми свойствами с уже присвоенными значениями по умолчанию. (Свойство name - свойство опционального типа String, значит значение по умолчанию равно nil). В примере выше используется дефолтный инициализатор для класса ShoppingListItem для создания нового экземпляра. Синтаксис дефолтного инициализатора в нашем случае выглядит как ShoppingListItem(), что присваивается переменной item.

Почленные инициализаторы структурных типов

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

Почленный инициализатор - сокращенный способ инициализировать свойства члена нового экземпляра структуры. Начальные значения для свойств нового экземпляра могут быть переданы в почленный инициализатор по имени.

Пример ниже определяет структуру Size с двумя свойствами width, height. Оба свойства выведены как Double из-за начального значения равного 0.0.

Структура Size автоматически получает init(width:height:) почленный инициализатор, который вы можете использовать для инициализации Size экземпляра:

struct Size {

var width = 0.0, height = 0.0

}

let twoByTwo = Size(width: 2.0, height: 2.0)

Делегирование инициализатора для типов значения

Инициализаторы могут вызывать другие инициализаторы для инициализации части экземпляра. Этот процесс называется как делегирование инициализатора. Он позволяет избегать дублирования кода в разных инициализаторах.

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

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

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

Заметка

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

Следующий пример определяет пользовательскую структуру Rect для отображения геометрического прямоугольника. Примеру нужно добавить две вспомогательные структуры Size, Point, каждая из которых предоставляет значения по умолчанию 0.0 для своих свойств:

struct Size {

var width = 0.0, height = 0.0

}

struct Point {

var x = 0.0, y = 0.0

}

Вы можете инициализировать структуру Rect тремя способами: используя свою нулевую инициализацию значений свойств origin и size, предоставляя определенную точку и размер, или предоставляя точку центра и размер. Эти опции инициализации представлены тремя инициализаторами, которые являются частью определения структуры Rect:

struct Rect {

var origin = Point()

var size = Size()

init() {}

init(origin: Point, size: Size) {

self.origin = origin

self.size = size

}

init(center: Point, size: Size) {

let originX = center.x - (size.width / 2)

let originY = center.y - (size.height / 2)

self.init(origin: Point(x: originX, y: originY), size: size)

}

}

Первый инициализатор Rect - init() функционально тот же самый, что и дефолтный инициализатор, который бы получила структура, если бы не имела пользовательских инициализаторов. Инициализатор имеет пустое тело, отображенное парой пустых фигурных скобок {}, и не проводит никакой инициализации. Вызывая такой инициализатор, мы возвращаем экземпляр Rect, который имеет инициализированные свойства origin, size значениями Point(x: 0.0, y: 0.0) и Size(width: 0.0, height: 0.0), которые известны из определения свойств:

let basicRect = Rect()

//исходная точка Rect (0.0, 0.0) и его размер (0.0, 0.0)

Второй инициализатор Rect - init(origin:size:) функционально то же самое что и почленный инициализатор, который могла бы иметь структура, если бы не имела пользовательских инициализаторов. Этот инициализатор просто присваивает значения аргументов origin, size соответствующим свойствам:

let originRect = Rect(origin: Point(x: 2.0, y: 2.0), size: Size(width: 5.0, height: 5.0))

//исходная точка Rect (2.0, 2.0) и его размер (5.0, 5.0)

Третий инициализатор Rect - init(center:size:) немного более сложный. Он начинается с вычисления соответствующей исходной точки, основываясь на точке center и значении size. Только потом он вызывает ( или делегирует) init(origin:size:) инициализатор, который хранит новую исходную точку и значения размеров соответствующих свойств:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0), size: Size(width: 3.0, height: 3.0))

//исходная точка centerRect’а равна (2.5, 2.5) и его размер (3.0, 3.0)

Инициализатор init(center:size:) мог бы присвоить новые значения origin, size соответствующим свойствам самостоятельно. Однако более удобно (т.к. более понятный из-за краткости) для инициализатора init(center:size:) воспользоваться преимуществом того, что существует другой инициализатор с абсолютно такой же функциональностью.

Заметка

Для альтернативного способа записи этого примера без инициализаторов init(), init(origin:size:) смотрите главу Расширения.

Наследование и инициализация класса

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

Swift определяет два вида инициализаторов классовых типов для проверки того, что все свойства получили какие-либо значения. Они известны как назначенные инициализаторы (конструкторы) или вспомогательные инициализаторы.

Назначенный и вспомогательный инициализатор

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

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

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

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

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

Синтаксис назначенных и вспомогательных инициализаторов

Назначенные инициализаторы для классов записываются точно так же как и простые инициализаторы для типов значений:

init(параметры) {

выражения

}

Вспомогательные инициализаторы пишутся точно так же, но только дополнительно используется вспомогательное слово convenience, которое располагается до слова init и разделяется пробелом:

convenience init(параметры) {

выражения

}

Делегирование инициализатора для классовых типов

Для простоты отношений между назначенными и вспомогательными инициализаторами, Swift использует следующие три правила для делегирования вызовов между инициализаторами:

Правило 1

Назначенный инициализатор должен вызывать назначенный инициализатор из суперкласса.

Правило 2

Вспомогательный инициализатор должен вызывать другой инициализатор из того же класса.

Правило 3

Вспомогательный инициализатор в конечном счете должен вызывать назначенный инициализатор.

Вот как можно просто это запомнить:

Назначенные инициализаторы должны делегировать наверх

Вспомогательные инициализаторы должны делегировать по своему уровню (классу).

Вот как это правило выглядит в иллюстрированной форме:

Здесь, в суперклассе есть один назначенный инициализатор и два вспомогательных инициализатора. Один вспомогательный инициализатор вызывает другой вспомогательный инициализатор, который в свою очередь вызывает единственный назначенный инициализатор. Этот рисунок удовлетворяет правилам 2 и 3. Этот суперкласс сам по себе уже дальше не имеет суперкласса, так что первое правило здесь не применимо.

Подкласс на этом рисунке содержит два назначенных инициализатора и один вспомогательный инициализатор. Вспомогательный инициализатор должен вызвать одного из двух назначенных инициализатора, потому что он может вызвать другой инициализатор того же класса. Это правило так же соответствует правилам 2 и 3. Оба назначенных инициализатора должны вызвать один назначенный инициализатор из суперкласса, для того чтобы соответствовать правилу 1.

Заметка

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

Схема ниже показывает более сложную иерархию из четырех классов. Она показывает как назначенные инициализаторы работают в качестве точек прохождения инициализации класса, упрощая внутренние взаимоотношения среди цепочки классов:

Двухфазная инициализация

Инициализация класса в Swift является двухфазным процессом. На первой фазе каждое свойство получает начальное значение от класса, в котором оно представлено. Как только первоначальные значения для  свойств хранения были определены, начинается вторая фаза, и каждому классу предоставляется возможность изменить свои свойства еще до того как будет считаться, что созданный экземпляр можно использовать.

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

Заметка

Двухфазный процесс инициализации в Swift аналогичен инициализации в Objective-C. Основное отличие между ними проходит на первой фазе в том, что в Objective-C свойства получают значения 0 или nil. В Swift же этот процесс более гибкий и позволяет устанавливать пользовательские начальные значения и может обработать типы, для которых значения 0 или nil, являются некорректными.

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

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

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

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

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

Проверка 4. Инициализатор не может вызывать методы экземпляра, читать значения любого свойства экземпляра или ссылаться на self как на значение до тех пор, пока не будет закончена первая фаза инициализации.

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

Вот как проходит двухфазная инициализация, основанная на четырех проверках(описанные выше):

Фаза первая

Назначенный или вспомогательный инициализатор вызывается в классе.

Память под новый экземпляр этого класса выделяется. Но она еще не инициализирована.

Назначенный инициализатор для этого класса подтверждает, что все свойства, представленные этим классом, имеют значения. Память под эти свойства теперь инициализирована.

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

Так продолжается по цепочке до самого верхнего суперкласса.

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

Фаза вторая

Двигаясь вниз по цепочке, каждый назначенный инициализатор в этой цепочке имеет такую возможность, как настраивать экземпляр. Теперь инициализаторы получают доступ к self и могут изменять свои свойства, создавать экземпляры и вызывать методы и т.д.

И наконец, каждый вспомогательный инициализатор в цепочки имеет возможность настраивать экземпляр и работать с self.

Вот как выглядит первая фаза для гипотетического подкласса и суперкласса:

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

Назначенный инициализатор убеждается, что все свойства, подкласса имеют значения, как указано в проверке 1. После этого он вызывает назначенный инициализатор своего суперкласса для продолжения инициализации.

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

Сразу после того как все свойства суперкласса получают начальные значения, память считается полностью инициализированной, Фаза 1 завершается.

Вот как выглядит Фаза 2:

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

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

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

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

Наследование и переопределение инициализатора

В отличии от подклассов в Objective-C, подклассы в Swift не наследуют инициализаторов их суперклассов по умолчанию. Такой подход в Swift предотвращает ситуации, когда простой инициализатор суперкласса наследуется более специфичным подклассом, а потом используется для создания экземпляра подкласса, который не полностью или не правильно инициализирован.

Заметка

Инициализаторы суперкласса наследуются в определенных обстоятельствах, но только когда это безопасно и когда это имеет смысл делать. Далее мы это разберем.

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

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

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

Заметка

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

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

Пример ниже определяет базовый класс Vehicle. Это базовый класс объявляет свойства numberOfWheels со значением 0 типа Int. Свойство numberOfWheels используется для вычисляемого свойства description, для создания описания характеристик транспортного средства типа String:

class Vehicle {

var numberOfWheels = 0

var description: String {

return "\(numberOfWheels) колес(о)"

}

}

Класс Vehicle предоставляет значение по умолчанию для его единственного свойства, и не имеет никаких собственных пользовательских инициализаторов. И в результате он автоматически получает дефолтный инициализатор, как описано в главе Дефолтные инициализаторы. Дефолтный инициализатор (когда доступен) всегда является назначенным инициализатором для класса и может быть использован для создания нового экземпляра класса Vehicle с numberOfWheels равным 0:

let vehicle = Vehicle()

print("Транспортное средство \(vehicle.description)")

//Транспортное средство 0 колес(о)

Следующий пример определяет подкласс Bicycle суперкласса Vehicle:

class Bicycle: Vehicle {

override init() {

super.init()

numberOfWheels = 2

}

}

Подкласс Bicycle определяет пользовательский назначенный инициализатор init(). Назначенный инициализатор совпадает с назначенным инициализатором из суперкласса Vehicle и, таким образом, версия этого инициализатора класса Bicycle отмечена модификатором override.

Инициализатор init() для Bicycle начинается с вызова super.init(), который в свою очередь вызывает дефолтный инициализатор для суперкласса Vehicle класса Bicycle. Он проверяет, что унаследованное свойство numberOfWheels инициализировано в Vehicle, после чего у Bicycle появляется возможность его модифицировать. После вызова super.init() начальное значение numberOfWheels заменяется значением 2.

Если вы создаете экземпляр Bicycle, вы можете вызвать его унаследованное вычисляемое свойство description, для того, чтобы посмотреть как обновилось свойство numberOfWheels:

let bicycle = Bicycle()

print("Велосипед: \(bicycle.description)")

//Велосипед: 2 колес(а)

Заметка

Подклассы могут менять унаследованные переменные свойства в процессе инициализации, но нельзя менять неизменяемые унаследованные свойства.

Автоматическое наследование инициализатора

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

Допуская, что вы предоставляете значения по умолчанию любому новому свойству, представленному в подклассе, то применяются два правила:

Правило 1. Если ваш подкласс не определяет ни одного назначенного инициализатора, он автоматически наследует все назначенные инициализаторы суперкласса.

Правило 2. Если у вашего класса есть реализация всех назначенных инициализаторов его суперкласса, либо они были унаследованы как по правилу 1 или же предоставлены как часть пользовательской реализации определения подкласса, то тогда этот подкласс автоматически наследует все вспомогательные инициализаторы суперкласса.

Эти правила применимы даже если ваш подкласс позже добавляет вспомогательные инициализаторы.

Заметка

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

Назначенные и вспомогательные инициализаторы в действии

Следующий пример показывает назначенные и вспомогательные инициализаторы, и автоматическое наследование инициализатора в действии. Этот пример определяет иерархию трех классов Food, RecipeIngredient и ShoppingListItem и демонстрирует как их инициализаторы взаимодействуют.

Основной (базовый) класс называется Food, который имеет одно простое свойство типа String, называемое name, и обеспечивает два инициализатора для создания экземпляров класса Food:

class Food {

var name: String

init(name: String) {

self.name = name

}

convenience init() {

self.init(name: "[Unnamed]")

}

}

Схема ниже показывает цепочку работы инициализаторов в классе Food :

Классы по умолчанию не имеют почленного инициализатора, так что класс Food предоставляет назначенный инициализатор, который принимает единственный аргумент name. Этот инициализатор может быть использован для создания экземпляра Food со специфичным именем:

let namedMeat = Food(name: "Бекон")

//имя namedMeat является "Бекон"

Инициализатор init(name: String) из класса Food, представлен в виде назначенного инициализатора, потому что он проверяет, что все хранимые свойства нового экземпляра Food полностью инициализированы. Класс Food не имеет суперкласса, так что инициализатор init(name: String) не имеет вызова super.init() для завершения своей инициализации.

Класс Food так же обеспечивает вспомогательный инициализатор init() без аргументов. Инициализатор init() предоставляет имя плейсхолдера для новой еды, делегируя к параметру name инициализатора init(name: String), давая ему значение [Unnamed] :

let mysteryMeat = Food()

//mysteryMeat называется "[Unnamed]"

Второй класс в иерархии - это подкласс RecipeIngredient класса Food. Класс RecipeIngredient создает модель ингредиентов в рецепте. Он представляет свойство quantity типа Int (в дополнение к свойству name, унаследованное от Food) и определяет два инициализатора для создания экземпляров RecipeIngredient :

class RecipeIngredient: Food {

var quantity: Int

init(name: String, quantity: Int) {

self.quantity = quantity

super.init(name: name)

}

override convenience init(name: String) {

self.init(name: name, quantity: 1)

}

}

Схема ниже показывает цепочку инициализаторов для класса RecipeIngredient:

Класс RecipeIngredient имеет один назначенный инициализатор init(name: String, quantity: Int), который может быть использован для установки значений всем свойствам нового экземпляра RecipeIngredient. Этот инициализатор начинается с присваивания переданного аргумента quantity свойству quantity, которое является единственным новым свойством представленным в RecipeIngredient. После того как это сделано, инициализатор делегирует вверх инициализатор init(name: String) для класса Food. Этот процесс удовлетворяет проверке №1 из раздела “Двухфазная инициализация”, что находится выше на этой же странице.

RecipeIngredient так же определяет вспомогательный инициализатор init(name: String), который используется для создания экземпляра RecipeIngredient только по имени. Этот вспомогательный инициализатор присваивает значение количество равное 1 для любого экземпляра, которое создано без явного указания количества. Определение этого вспомогательного инициализатора ускоряет создание экземпляров класса RecipeIngredient и позволяет избежать повторения кода при создании экземпляра, где свойство quantity изначально всегда равно 1. Этот вспомогательный инициализатор делегирует по назначенному инициализатору класса, передавая ему quantity равное 1.

Вспомогательный инициализатор init(name: String) предоставленный RecipeIngredient’ом принимает те же параметры, что и назначенный инициализатор init(name: String) в Food. Из-за того, что вспомогательный инициализатор переопределяет назначенный инициализатор из своего суперкласса, то он должен быть обозначен ключевым словом override.

Даже если RecipeIngredient представляет инициализатор init(name: String) как вспомогательный инициализатор, то RecipeIngredient тем не менее проводит реализацию всех назначенных инициализаторов своего суперкласса. Таким образом RecipeIngredient автоматически наследует все свойства вспомогательных инициализаторов своего суперкласса тоже.

В этом примере суперкласс для класса RecipeIngredient является Food, который имеет единственный инициализатор init(). Поэтому этот инициализатор наследуется RecipeIngredient. Наследованная версия init() функционирует абсолютно так же как и версия в Food, за исключением того, что она делегирует в RecipeIngredient версию init(name: String), а не в версию Food.

Все три инициализатора могут быть использованы для создания новых RecipeIngredient экземпляров:

let oneMysteryItem = RecipeIngredient()

let oneBacon = RecipeIngredient(name: "Bacon")

let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

Третий и последний класс в иерархии - подкласс ShoppingListItem класса RecipeIngredient. ShoppingListItem может создавать рецепты из ингредиентов, как только они появляются в листе покупок.

Каждый элемент в листе покупок (shopping list) начинается с “не куплен” или “unpurchased”. Для отображения того факта, что ShoppingListItem представляет булево свойство purchased, со значением по умолчанию false. ShoppingListItem так же добавляет высчитываемое свойство description, которое предоставляет текстовое описание экземпляра ShoppingListItem :

class ShoppingListItem: RecipeIngredient {

var purchased = false

var description: String {

var output = "\(quantity) x \(name)"

output += purchased ? " " : " ✘"

return output

}

}

Заметка

ShoppingListItem не определяет инициализатор для предоставления исходного значения для purchased, потому что элементы в листе покупок, как смоделировано тут, сначала имеют значение false, то есть они не куплены.

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

Схема ниже отображает общую цепочку инициализаций для всех трех классов:

Вы можете использовать все три унаследованных инициализатора для создания нового экземпляра ShoppingListItem :

var breakfastList = [

ShoppingListItem(),

ShoppingListItem(name: "Bacon"),

ShoppingListItem(name: "Eggs", quantity: 6),

]

breakfastList[0].name = "Orange juice"

breakfastList[0].purchased = true

for item in breakfastList {

print(item.description)

}

// 1 x Orange juice

// 1 x Bacon ✘

// 6 x Eggs ✘

Здесь мы создаем новый массив breakfastList, заполняя его тремя экземплярами класса ShoppingListItem. Тип массива выводится из [ShoppingListItem]. После того как массив создан, мы меняем исходное имя с "[Unnamed]" на "Orange juice" и присваиваем свойству purchased значение true. Потом мы выводим на экран описание каждого элемента массива, где мы можем видеть, что все начальные значения установлены так как мы и ожидали.

Проваливающиеся инициализаторы

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

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

Заметка

Вы не можете определить проваливающийся инициализатор и обычные инициализаторы с одними и теми же именами и типами параметров.

Проваливающийся инициализатор создает опциональное значение типа, который он инициализирует. Вы пишите return nil внутри проваливающегося инициализатора для индикации точки, где инициализация может провалиться.

Заметка

Строго говоря, инициализаторы не возвращают значений. Их роль заключается в том, что они проверяют, что self полностью и корректно инициализирован, до того, как инициализация закончится. Несмотря на то, что вы пишите return nil для указания неудачи инициализации, вы не пишите слово return в случае, если инициализация прошла успешно.

Например, проваливающиеся инициализаторы реализуются для преобразования числового типа. Для гарантии того, что преобразование между числовыми типами имеет смысл, используйте инициализатор  init(exactly:). Если преобразование невозможно, то данный инициализатор "провалится", то есть вернет nil.

let wholeNumber: Double = 12345.0

let pi = 3.14159

if let valueMaintained = Int(exactly: wholeNumber) {

print("\(wholeNumber) преобразование в Int поддерживает значение \(valueMaintained)")

}

// Prints "12345.0 conversion to Int maintains value of 12345"

let valueChanged = Int(exactly: pi)

// valueChanged is of type Int?, not Int

if valueChanged == nil {

print("\(pi) преобразование в Int невозможно")

}

// Выведет "3.14159 преобразование в Int невозможно"

Пример ниже определяет структуру Animal, с константным свойством типа String с именем species. Структура Animal так же определяет проваливающийся инициализатор с одним параметром species. Этот инициализатор проверяет было ли передано значение из species в инициализатор, если оно равно nil, то срабатывает проваливающийся инициализатор. В противном случае значение свойства species установлено и инициализация проходит успешно.

struct Animal {

let species: String

init?(species: String) {

if species.isEmpty { return nil }

self.species = species

}

}

Вы можете использовать этот проваливающий инициализатор для попытки инициализировать новый экземпляр структуры Animal и проверить успешно ли прошла инициализация:

let someCreature = Animal(species: "Жираф")

// someCreature имеет тип Animal?, но не Animal

if let giraffe = someCreature {

print("Мы инициализировали животное типа \(giraffe.species) ")

}

// Выведет "Мы инициализировали животное типа Жираф "

Если вы передаете пустую строку в параметр species проваливающегося инициализатора, то инициализатор вызывает сбой инициализации:

let anonymousCreature = Animal(species: "")

// someCreature имеет тип Animal?, но не Animal

if anonymousCreature == nil {

print("Неизвестное животное не может быть инициализировано")

}

// Выведет "Неизвестное животное не может быть инициализировано"

Заметка

Проверяя значение пустой строки (к примеру "", а не "Жираф") это не тоже самое, что проверять на nil, для индикации отсутствия значения опционального String. В примере выше, пустая строка ("") корректна и является обычной String, а не String?. Однако это не допустимо в нашем случае, чтобы животное имело пустое значение, например, свойства species. Для того чтобы смоделировать такое ограничение, мы используем проваливающийся инициализатор, который выдает сбой, если находит пустую строку.

Проваливающиеся инициализаторы для перечислений

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

Пример ниже определяет перечисление TemperatureUnit с тремя возможными вариантами (kelvin, celsius и fahrenheit). Проваливающийся инициализатор используется для того, чтобы найти подходящий член перечисления для значения типа Character, которое представляет символ температуры:

enum TemperatureUnit {

case kelvin, celsius, fahrenheit

init?(symbol: Character) {

switch symbol {

case "K":

self = .kelvin

case "C":

self = .celsius

case "F":

self = .fahrenheit

default:

return nil

}

}

}

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

let fahrenheitUnit = TemperatureUnit(symbol: "F")

if fahrenheitUnit != nil {

print("Эта единица измерения температура определена, а значит наша инициализация прошла успешно!")

}

// Выведет "Эта единица измерения температура определена, а значит наша инициализация прошла успешно!"

let unknownUnit = TemperatureUnit(symbol: "X")

if unknownUnit == nil {

print("Единица измерения температуры не определена, таким образом мы зафейлили инициализацию")

}

// Выведет "Единица измерения температуры не определена, таким образом мы зафейлили инициализацию"

Проваливающиеся инициализаторы для перечислений с начальными значениями

Перечисления с начальными значениями по умолчанию получают проваливающийся инициализатор init?(rawValue:), который принимает параметр rawValue подходящего типа и выбирает соответствующий член перечисления, если он находит подходящий, или срабатывает сбой инициализации, если существующее значение не находит совпадения среди членов перечисления.

Вы можете переписать пример TemperatureUnit из примера выше для использования начальных значений типа Character и использовать инициализатор init?(rawValue:):

enum TemperatureUnit: Character {

case kelvin = "K", celsius = "C", fahrenheit = "F"

}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")

if fahrenheitUnit != nil {

print("Эта единица измерения температура определена, а значит наша инициализация прошла успешно!")

}

// Выведет "Эта единица измерения температура определена, а значит наша инициализация прошла успешно!"

let unknownUnit = TemperatureUnit(rawValue: "X")

if unknownUnit == nil {

print("Единица измерения температуры не определена, таким образом мы зафейлили инициализацию.")

}

// Выведет "Единица измерения температуры не определена, таким образом мы зафейлили инициализацию."

Распространение проваливающегося инициализатора

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

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

Заметка

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

Пример ниже определяет подкласс CartItem класса Product. CartItem создает модель элемент в корзине онлайн заказа. CartItem представляет свойство хранения quantity и проверяет, чтобы это свойство всегда имело значение не менее 1:

class Product {

let name: String

init?(name: String) {

if name.isEmpty { return nil }

self.name = name

}

}

class CartItem: Product {

let quantity: Int

init?(name: String, quantity: Int) {

if quantity < 1 { return nil }

self.quantity = quantity

super.init(name: name)

}

}

Проваливающийся инициализатор для CartItem начинается с того, что получает значение quantity 1 или более. Если значение quantity некорректное, то вся инициализация проваливается и код дальше не исполняется. Так же проваливающийся инициализатор Product проверяет значение свойства name, и если оно равно пустой строке, то инициализация немедленно прекращается.

Если вы создаете экземпляр CartItem с name не равной пустой строке и quantity равному 1 или более, то инициализация проходит успешно:

if let twoSocks = CartItem(name: "sock", quantity: 2) {

print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")

}

// Выведет "Item: sock, quantity: 2"

Если вы попытаетесь создать экземпляр CartItem с quantity со значением 0, то инициализация провалится:

if let zeroShirts = CartItem(name: "shirt", quantity: 0) {

print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")

} else {

print("Невозможно инициализировать ноль футболок")

}

// Выведет "Невозможно инициализировать ноль футболок"

Аналогично, если вы попытаетесь создать экземпляр CartItem с name равным пустой строке, то инициализатор суперкласса Product вызовет неудачу инициализации:

if let oneUnnamed = CartItem(name: "", quantity: 1) {

print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")

} else {

print("Невозможно инициализировать товар без имени")

}

// Выведет "Невозможно инициализировать товар без имени"

Переопределение проваливающегося инициализатора

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

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

Заметка

Вы можете переопределить проваливающийся инициализатор непроваливающимся инициализатором, но не наоборот.

Пример ниже определяет класс Document. Этот класс моделирует документ, который может быть инициализирован свойством name, которое может иметь значение отличное от пустого или nil, но никак не пустую строку:

class Document {

var name: String?

//этот инициализатор создает документ со значением nil свойства name

init(){}

//этот инициализатор создает документ с не пустым свойством name

init?(name: String) {

if name.isEmpty { return nil }

self.name = name

}

}

Следующий пример определяет подкласс AutomaticallyNamedDocument класса Document. AutomaticallyNamedDocument является подклассом, который переопределяет оба назначенных инициализатора, представленных в Document. Это переопределение гарантирует, что экземпляр AutomaticallyNamedDocument будет иметь исходное значение "[Untitled]" свойства name, если экземпляр создан без имени или если пустая строка передана в инициализатор init(name:):

class AutomaticallyNamedDocument: Document {

override init() {

super.init()

self.name = "[Untitled]"

}

override init(name: String) {

super.init()

if name.isEmpty {

self.name = "[Untitled]"

} else {

self.name = name

}

}

}

AutomaticallyNamedDocument переопределяет проваливающийся инициализатор init?(name:) суперкласса непроваливающимся инициализатором init(name:). Из-за того, что AutomaticallyNamedDocument справляется с пустой строкой иначе, чем его суперкласс, его инициализатор не обязательно должен провалиться, таким образом он предоставляет непроваливающуюся версию инициализатора вместо проваливающейся.

Вы можете использовать принудительное извлечение внутри инициализатора для вызова проваливающегося инициализатора из суперкласса, в качестве части реализации непроваливающегося инициализатора подкласса. Например, подкласс класса UntitledDocument всегда имеет имя "[Untitled]", и он использует проваливающийся init(name:) из суперкласса во время инициализации.

class UntitledDocument: Document {

override init() {

super.init(name: "[Untitled]")!

}

}

В этом случае инициализатор суперкласса init(name:) каждый раз будет вызывать ошибку исполнения, если в него будет передана пустая строка. Однако, так как этот инициализатор теперь имеет строковую константу, то этот инициализатор больше не провалится, то есть ошибки исполнения больше не будет.

Проваливающийся инициализатор init!

Обычно вы определяете проваливающийся инициализатор, который создает опциональный экземпляр соответствующего типа путем размещения знака вопроса после ключевого слова init (init?). Альтернативно, вы можете определить проваливающийся инициализатор, который создает экземпляр неявно извлекаемого опционала соответствующего типа. Сделать это можно, если вместо вопросительного знака поставить восклицательный знак после ключевого слова init (init!).

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

Требуемые инициализаторы

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

class SomeClass {

required init() {

//пишем тут реализацию инициализатора

}

}

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

class SomeSubclass: SomeClass {

required init() {

//пишем тут реализацию инициализатора подкласса

}

}

Заметка

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

Начальное значение свойства в виде функции или замыкания

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

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

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

class SomeClass {

let someProperty: SomeType = {

// создаем начальное значения для SomeProperty внутри этого замыкания

// someValue должен быть того же типа, что и SomeType

return someValue

}()

}

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

Заметка

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

Пример ниже определяет структуру Checkerboard, которая моделирует доску для игры “Шашки”:

В игру шашки играют на доске 8х8, с чередующимися черными и белыми клетками. Для отображения игральной доски для этой игры, структура Checkerboard имеет единственное свойство boardColors, которое является массивом из 64 значений типа Bool. Значение true отображает черные клетки, false - белые. Первое значение массива отображает левый верхний угол доски, значение последнего элемента отображает правый нижний угол доски.

Массив boardColors инициализирован с помощью замыкания для установки значений цветов:

struct Chessboard {

let boardColors: [Bool] = {

var temporaryBoard = [Bool]()

var isBlack = false

for i in 1...8 {

for j in 1...8 {

temporaryBoard.append(isBlack)

isBlack = !isBlack

}

isBlack = !isBlack

}

return temporaryBoard

}()

func squareIsBlackAt(row: Int, column: Int) -> Bool {

return boardColors[(row * 8) + column]

}

}

Когда бы вы не создали новый экземпляр Checkerboard, замыкание исполняется, и начальное значение boardColors высчитывается и возвращается. Замыкание в примере выше высчитывает временный массив temporaryBoard и возвращает его в качестве возвращаемого значения замыканием, как только он весь заполняется. Возвращенный массив сохраняется в boardColors и может быть запрошен через метод squareIsBlackAt:

let board = Chessboard()

print(board.squareIsBlackAt(row: 0, column: 1))

// Выведет "true"

print(board.squareIsBlackAt(row: 7, column: 7))

// Выведет "false"

Деинициализация

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

Как работает деинициализация

Swift автоматически освобождает ваши экземпляры, что освобождает память в свою очередь, когда они больше не нужны. Swift берет на себя управление памятью экземпляров через ARC(automatic reference counting), что будет объяснено в разделе Автоматический подсчет ссылок. Обычно вам не нужно вручную чистить память, когда ваши экземпляры освобождаются. Однако, когда вы работаете с вашими собственными ресурсами, вам, возможно, понадобится проводить дополнительную чистку. К примеру, если вы создаете свой класс для открытия файла и записи в него какой-то информации, а потом его закрываете, то вам понадобится закрыть файл до того как вы освободите экземпляр класса.

Определения класса могут иметь максимум один деинициализатор на один класс. Деинициализатор не принимает ни одного параметра и пишется без круглых скобок:

deinit {

// проведение деинициализации

}

Деинициализаторы вызываются автоматически прямо перед тем как освобождается экземпляр. У вас нет возможности вызывать деинициализатор самостоятельно. Деинициализаторы суперкласса наследуются их подклассами, и деинициализаторы суперкласса вызываются автоматически в конце реализации деинициализатора подкласса. Деинициализаторы суперклассов всегда вызываются, даже если подкласс не имеет своего деинициализатора.

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

Деинициализаторы в действии

Здесь приведен пример деинициализатора в действии. Для создания простой игры в этом примере определяются два новых типа Bank и Player. Структура Bank управляет наличностью, которой не может быть больше 10000 монет в обороте. В игре может быть только один Bank, так что Bank реализован в качестве класса со свойствами типа и методами хранения и управления текущим состоянием:

class Bank {

static var coinsInBank = 10_000

static func distribute(coins numberOfCoinsRequested: Int) -> Int {

let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)

coinsInBank -= numberOfCoinsToVend

return numberOfCoinsToVend

}

static func receive(coins: Int) {

coinsInBank += coins

}

}

Bank следит за текущим количеством монет, которые у него есть в свойстве coinsInBank. Так же он предоставляет два метода distribute(coins:), receive(coins:) для обработки событий коллекционирования монет и их распределения.

Метод distribute(coins:) проверяет достаточность количества монет в банке перед их дистрибуцией. Если в банке недостаточно монет, то Bank возвращает меньшее число, чем было запрошено (и возвращает ноль, если монет в банке не осталось). Он возвращает целое число, показывая сколько монет было предоставлено.

Метод receive(coins:) просто добавляет полученное число монет обратно в банк.

Класс Player описывает игрока в игре. Каждый игрок имеет определенное количество монет, хранящихся для любых целей, которые могут быть использованы в любое время. Это отображается свойством coinsInPurse:

class Player {

var coinsInPurse: Int

init(coins: Int) {

coinsInPurse = Bank.distribute(coins: coins)

}

func win(coins: Int) {

coinsInPurse += Bank.distribute(coins: coins)

}

deinit {

Bank.receive(coins: coinsInPurse)

}

}

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

Класс Player определяет метод win(coins:), который получает определенное число монет от банка и добавляет их в кошелек игрока. Класс Player так же реализует деинициализатор, который вызывается сразу после того, как экземпляр освобождается. В примере ниже деинициализатор просто возвращает все монеты игрока в банк:

var playerOne: Player? = Player(coins: 100)

print("A new player has joined the game with \(playerOne!.coinsInPurse) coins")

// Выведет "A new player has joined the game with 100 coins"

print("There are now \(Bank.coinsInBank) coins left in the bank")

// Выведет "There are now 9900 coins left in the bank"

Новый экземпляр Player создан со 100 монетами, если такое количество есть в банке. Экземпляр хранит опциональную переменную playerOne типа Player. Опциональный тип используется здесь, поскольку игрок может покинуть игру в любой момент. Опционал позволяет вам отслеживать присутствие игрока в игре.

Так как playerOne является опционалом, то используем восклицательный знак (!), когда мы хотим получить доступ к его свойству coinsInPurse или когда вызываем метод winCoins(_:):

playerOne!.win(coins: 2_000)

print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")

// Выведет "PlayerOne won 2000 coins & now has 2100 coins"

print("The bank now only has \(Bank.coinsInBank) coins left")

// Выведет "The bank now only has 7900 coins left"

Здесь наш игрок выиграл 2000 монет. Теперь у него в кошельке 2100 монет, соответственно, в банке осталось 7900 монет.

playerOne = nil

print("PlayerOne has left the game")

// Выведет "PlayerOne has left the game"

print("The bank now has \(Bank.coinsInBank) coins")

// Выведет "The bank now has 10000 coins"

Игрок покинул игру. Это осуществимо, если мы присвоим опционалу playerOne значение nil, что значит, что игрока больше нет. На этот момент мы больше не имеем доступа к свойствам или методам переменной playerOne, то есть у нас не стало ссылки на экземпляр класса Player, так что экземпляр освобождается и освобождается память. Прямо перед тем как это случится, автоматически вызывается деинициализатор, который возвращает монеты в банк.

Опциональная последовательность

Опциональная цепочка (optional chaining) - процесс запросов и вызовов свойств, методов, сабскриптов (индексов) у опционала, который может быть nil. Если опционал содержит какое-либо значение, то вызов свойства, метода или сабскрипта успешен, и наоборот, если опционал равен nil, то вызов свойства, метода или сабскрипта возвращает nil. Множественные запросы могут быть соединены вместе, и вся цепочка этих запросов не срабатывает, если хотя бы один запрос равен nil.

Заметка

Опциональная последовательность в Swift - аналог сообщению nil в Objective-C, но только она работает со всеми типами, и может быть проверена на успех или неудачу.

ОП как альтернатива принудительному извлечению

Вы обозначаете опциональную последовательность, когда ставите вопросительный знак (?) опционального значения, свойство, метод или индекс которого вы хотите вызвать, если опционал не nil. Это очень похоже на установку восклицательного знака (!) после опционального значения для принудительного извлечения его значения. Основное отличие в том, что опциональная последовательность не исполняется, если опционал равен nil, в то время как принудительное извлечение приводит к runtime ошибке, когда опционал равен nil.

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

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

Следующие несколько фрагментов кода покажут вам как отличаются опциональная последовательность от принудительного извлечения, и как она позволяет вам проверить значение на успех.

Первые два класса Person, Residence определены как:

class Person {

var residence: Residence?

}

class Residence {

var numberOfRooms = 1

}

Экземпляры Residence имеют единственное свойство numberOfRooms типа Int, со значением по умолчанию 1. Экземпляры Person имеют опциональное свойство residence типа Residence?.

Если вы создаете новый экземпляр Person, то его свойство residence по умолчанию имеет значение nil, в силу того, что оно является опционалом. В коде ниже john имеет свойство residence, значение которого nil:

let john = Person()

Если вы попытаетесь получить доступ к свойству numberOfRooms свойства residence экземпляра Person, поставив восклицательный знак после residence, для принудительного извлечения, то вы получите ошибку исполнения, потому что residence не имеет значения для извлечения:

let roomCount = john.residence!.numberOfRooms

// ошибка runtime

Код, представленный выше, срабатывает успешно, если john.residence имеет не nil значение и устанавливает корректное значение типа Int для roomCount. Однако этот код всегда будет выдавать ошибку исполнения, когда residence равен nil, что указано выше.

Опциональная последовательность предоставляет альтернативный способ получить доступ к значению numberOfRooms. Для использования опциональной последовательности используйте вопросительный знак, на месте восклицательного знака:

if let roomCount = john.residence?.numberOfRooms {

print("John's residence has \(roomCount) room(s).")

} else {

print("Unable to retrieve the number of rooms.")

}

// Выведет "Unable to retrieve the number of rooms."

Это сообщает Swift “сцепиться” с опциональным свойством residence и получить значение numberOfRooms, если residence существует.

Так как попытка доступа к numberOfRooms имеет потенциальную возможность претерпеть неудачу, то опциональная последовательность возвращает значение типа Int? или “опциональный Int”. Когда residence равен nil, в примере выше, этот опциональный Int также будет nil, для отображения того факта, что было невозможно получить доступ к numberOfRooms. Необязательный Int получает доступ через необязательную привязку для разворачивания целого числа и присваивает неопциональное значение переменной roomCount.

Обратите внимание, что это верно даже если numberOfRooms неопциональный Int. Факт того, что запрос был через опциональную последовательность означает, что вызов numberOfRooms будет всегда возвращать Int?, вместо Int.

Вы можете присвоить экземпляр Residence в john.residence, так что оно больше не будет являться значением nil.

john.residence = Residence()

Теперь john.residence содержит экземпляр Residence, а не nil. Если вы попытаетесь получить доступ к numberOfRooms с той же последовательностью опционала, что и раньше, то теперь она вернет нам Int?, который содержит значение по умолчанию numberOfRooms равное 1:

if let roomCount = john.residence?.numberOfRooms {

print("John's residence has \(roomCount) room(s).")

} else {

print("Unable to retrieve the number of rooms.")

}

// Выведет "John's residence has 1 room(s)."

Определение классовых моделей для ОП

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

Фрагмент кода ниже определяет четыре модели классов для использования в нескольких следующих примерах, включая примеры с многоуровневой опциональной последовательностью. Эти классы расширяют модели Person, Residence приведенные ранее, добавляя классы Room, Address со свойствами, методами и сабскриптами.

Класс Person объявляется так же как и раньше:

class Person {

var residence: Residence?

}

Класс Residence стал намного сложнее, чем был раньше. В этот раз класс Residence определяет переменное свойство rooms, которое инициализировано пустым массивом [Room]:

class Residence {

var rooms = [Room]()

var numberOfRooms: Int {

return rooms.count

}

subscript(i: Int) -> Room {

get {

return rooms[i]

}

set {

rooms[i] = newValue

}

}

func printNumberOfRooms() {

print("Общее количество комнат равно \(numberOfRooms)")

}

var address: Address?

}

Из-за того, что эта версия Residence хранит массив экземпляров Room, его свойство numberOfRooms реализовано как вычисляемое, а не как хранимое свойство. Вычисляемое свойство numberOfRooms просто возвращает значение свойства.

В качестве сокращенного варианта доступа к массиву rooms, эта версия класса Residence предлагает сабскрипт (доступный как для чтения, так и для записи), который предоставляет доступ к комнате под требуемым индексом в массиве rooms.

Эта версия Residence так же обеспечивает метод printNumberOfRooms, который просто выводит на экран количество комнат в резиденции.

И наконец, Residence определяет опциональное свойство address типа Address?. Тип класса Address для этого свойства определен ниже.

Класс Room используется для массива rooms, в качестве простого класса с одним свойством name и инициализатором, в котором устанавливается значение свойства name как подходящее имя комнаты:

class Room {

let name: String

init(name: String) { self.name = name }

}

Последний класс в этой модели Address. Этот класс имеет три опциональных свойства типа String?. Первые два свойства buildingName, buildingNumber являются альтернативным вариантом определения конкретного здания как части адреса. Третье свойство street используется для названия улицы, для этого адреса:

class Address {

var buildingName: String?

var buildingNumber: String?

var street: String?

func buildingIdentifier() -> String? {

if let buildingNumber = buildingNumber, let street = street {

return "\(buildingNumber) \(street)"

} else if buildingName != nil {

return buildingName

} else {

return nil

}

}

}

Класс Address так же предоставляет метод buildingIdentifier(), который возвращает String?. Этот метод проверяет значения свойств экземпляра и возвращает buildingName, если у него есть значение или возвращает buildingNumber, если у него есть значение или nil, если ни у одного из свойств нет значения.

Доступ к свойствам через ОП

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

Используйте классы, определенные ранее, для создания нового экземпляра Person и попробуйте получить доступ к свойству numberOfRooms, как вы делали ранее:

let john = Person()

if let roomCount = john.residence?.numberOfRooms {

print("John's residence has \(roomCount) room(s).")

} else {

print("Unable to retrieve the number of rooms.")

}

// Выведет "Unable to retrieve the number of rooms."

Так как john.residence равен nil, этот вызов опциональной последовательности не будет успешен как и ранее.

Вы можете попробовать установить значение свойства через опциональную последовательность:

let someAddress = Address()

someAddress.buildingNumber = "29"

someAddress.street = "Acacia Road"

john.residence?.address = someAddress

В этом примере попытка установить значение свойству address опциональному свойству residence? провалится, так как john.residence все еще nil.

Присваивание является частью опциональной цепочки, что означает, что никакой код справой стороны не вычисляется. В предыдущем примере не так просто заметить, что someAddress никогда не вычисляется, потому что доступ к самой константе не имеет никаких побочных эффектов. Пример ниже делает тоже самое присваивание, но он использует функцию для того, чтобы создать адрес. Функция выводит "Function was called" до того, как вернется значение, что позволяет вам увидеть была ли вычислена правая часть от оператора присваивания.

func createAddress() -> Address {

print("Function was called.")

let someAddress = Address()

someAddress.buildingNumber = "29"

someAddress.street = "Acacia Road"

return someAddress

}

john.residence?.address = createAddress()

Можно заметить, что функция createAddress() не вызывается, так как ничего в консоли у нас не выводится.

Вызов методов через ОП

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

Метод printNumberOfRooms класса Residence выводит текущее значение numberOfRooms. Вот как выглядит этот метод:

func printNumberOfRooms() {

print("Общее количество комнат равно \(numberOfRooms)")

}

Этот метод не определяет возвращаемого значения. Однако функции и методы без возвращаемого значения имеют неявный возвращаемый тип Void, как было описано в главе “Функции, не возвращающие значения”. Это означает, что они возвращают значение () или просто пустой кортеж.

Если вы вызовите этот метод на опциональном значении в опциональной последовательности, то он вернет тип не Void, а Void?, потому что возвращаемые значения всегда опционального типа, когда они вызываются через опциональную последовательность. Это позволяет вам использовать конструкцию if для проверки на возможность вызова метода printNumberOfRooms, даже если метод сам не определяет возвращаемого значения. Сравните возвращаемое значение от вызова printNumberOfRooms и nil, для того чтобы увидеть, что вызов метода прошел успешно:

if john.residence?.printNumberOfRooms() != nil {

print("Есть возможность вывести общее количество комнат.")

} else {

print("Нет возможности вывести общее количество комнат.")

}

// Выведет "Нет возможности вывести общее количество комнат."

То же самое верно, если вы попытаетесь установить свойство через опциональную последовательность. Пример выше в “Доступ к свойствам через опциональную последовательность” пытается установить значение address в john.residence, хотя свойство residence равно nil. Любая попытка установить свойство через опциональную последовательность возвращает значение Void?, которое позволяет вам сравнивать его с nil, для того, чтобы увидеть логический результат установки значения свойству (успех, провал):

if (john.residence?.address = someAddress) != nil {

print("Была возможность установить адрес.")

} else {

print("Не было возможности установить адрес.")

}

// Выведет "Не было возможности установить адрес."

Доступ к сабскриптам через ОП

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

Заметка

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

Пример ниже пробует получить значение имени первой комнаты в массиве rooms свойства john.residence, используя сабскрипт, определенный в классе Residence. Из-за того, что john.residence является nil, то вызов сабскрипта проваливается:

if let firstRoomName = john.residence?[0].name {

print("Название первой комнаты \(firstRoomName).")

} else {

print("Никак не получить название перовой комнаты.")

}

// Выведет "Никак не получить название перовой комнаты."

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

Аналогично вы можете попробовать установить новое значение через сабскрипт с помощью опциональной последовательности:

john.residence?[0] = Room(name: "Bathroom")

Это попытка установки значения через сабскрипт так же не срабатывает, так как residence все еще nil.

Если вы создадите и присвоите действительное значение экземпляру Residence, при помощи одного или нескольких экземпляров Room в массиве rooms, то вы сможете использовать сабскрипт на экземпляре residence для того, чтобы получить доступ к массиву rooms через опциональную последовательность:

let johnsHouse = Residence()

johnsHouse.rooms.append(Room(name: "Гостиная"))

johnsHouse.rooms.append(Room(name: "Кухня"))

john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {

print("Название первой комнаты \(firstRoomName).")

} else {

print("Никак не получить название перовой комнаты.")

}

// Выведет "Название первой комнаты Гостиная."

Получение доступа к сабскрипту (индексу) опционального типа

Если сабскрипт возвращает значение опционального типа, например ключ словаря типа Dictionary в Swift, то мы должны поставить вопросительный знак после закрывающей скобки сабскрипта, для присоединения его опционального возвращаемого значения:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]

testScores["Dave"]?[0] = 91

testScores["Bev"]?[0] += 1

testScores["Brian"]?[0] = 72

// массив "Dave" теперь имеет вид [91, 82, 84], массив "Bev" - [80, 94, 81]

Пример выше определяет словарь testScores, который содержит две пары ключ/значение, которые соединяют ключ типа String со значением типа [Int]. Пример использует опциональную последовательность для установки значения первого элемента ключа "Dave" равным 91, для увеличения первого элемента массива под ключом "Bev" на 1 и для попытки установить первое значение несуществующего массива, соответствующего ключу "Brian" равным 72. Первые два вызова завершились успешно, потому что их ключи находятся в testScores. Третий вызов завершился неудачей, так как такого ключа как "Brian" в словаре не оказалось.

Соединение нескольких уровней ОП

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

Скажем другими словами:

Если тип, который вы пытаетесь получить не опциональный, то он станет опциональным из-за опциональной последовательности.

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

Таким образом:

Если вы пытаетесь получить значение типа Int через опциональную последовательность, то получите Int?, и это не будет зависеть от того, сколько уровней в опциональной последовательности задействовано.

Аналогично, если вы попытаетесь получить значение типа Int? через опциональную последовательность, то вы получите Int?, что опять таки не зависит от количества уровней, которые задействованы в опциональной последовательности.

Пример ниже пробует получить доступ к свойству street свойства address свойства residence экземпляра john. Здесь задействовано два уровня опциональной последовательности для того, чтобы соединить свойства residence и address, оба из которых опционального типа:

if let johnsStreet = john.residence?.address?.street {

print("John's street name is \(johnsStreet).")

} else {

print("Unable to retrieve the address.")

}

// Выведет "Unable to retrieve the address."

Значение john.residence на данный момент содержит корректный экземпляр класса Residence. Однако значение john.residence.address равно nil. Из-за этого вызов john.residence?.address?.street проваливается.

Обратите внимание, что в примере выше вы пытаетесь получить значение свойства street. Тип этого свойства String?. Возвращаемое значение john.residence?.address?.street так же String?, даже если два уровня опциональной последовательности применены в дополнение к опциональному типу самого свойства.

Если вы установите фактический экземпляр класса Address как значение для john.residence.address и установите фактическое значение для свойства street, то вы можете получить доступ к значению свойства street через многоуровневую опциональную последовательность (цепочку):

let johnsAddress = Address()

johnsAddress.buildingName = "The Larches"

johnsAddress.street = "Laurel Street"

john.residence?.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {

print("John's street name is \(johnsStreet).")

} else {

print("Unable to retrieve the address.")

}

// Выведет "John's street name is Laurel Street."

В этом примере попытка установить свойство address свойства john.residence будет успешной, потому что значение john.residence в настоящее время содержит действующий экземпляр Address.

Связывание методов в ОП с опциональными возвращаемыми значениями

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

Пример ниже вызывает метод buildingIdentifier класса Address через опциональную последовательность. Этот метод возвращает значение типа String?. Как было описано ранее, что возвращаемый тип этого метода после опциональной последовательности так же будет String?:

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {

print("John's building identifier is \(buildingIdentifier).")

}

// Выведет "John's building identifier is The Larches."

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

if let beginsWithThe =

john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {

if beginsWithThe {

print("John's building identifier begins with \"The\".")

} else {

print("John's building identifier does not begin with \"The\".")

}

}

// Выведет "John's building identifier begins with "The"."

Заметка

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

Обработка ошибок

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

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

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

Заметка

Обработка ошибок в Swift перекликается с шаблонами обработки ошибок, которые используются в классе NSError в Cocoa и Objective-C. Для более подробной информации читайте Error Handling и Using Swift With Cocoa and Objective-C (Swift 4).

Отображение и генерация ошибок

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

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

enum VendingMachineError: Error {

case invalidSelection

case insufficientFunds(coinsNeeded: Int)

case outOfStock

}

Генерация ошибки позволяет указать, что произошло что-то неожиданное и обычное выполнение программы не может продолжаться. Для того чтобы «сгенерировать» ошибку, вы используете инструкцию throw. Например, следующий код генерирует ошибку, указывая, что пять дополнительных монет нужны торговому автомату:

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

Обработка ошибок

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

В Swift существует четыре способа обработки ошибок. Вы можете передать (propagate) ошибку из функции в код, который вызывает саму эту функцию, обработать ошибку, используя инструкцию do-catch, обработать ошибку, как значение опционала, или можно поставить утверждение, что ошибка в данном случае исключена. Каждый вариант будет рассмотрен далее.

Когда функция генерирует ошибку, последовательность выполнения вашей программы меняется, поэтому важно сразу обнаружить место в коде, которое может генерировать ошибки. Для того, чтобы выяснить где именно это происходит, напишите ключевое слово try - или варианты try? или try!- до куска кода, вызывающего функцию, метод или инициализатор, который может генерировать ошибку. Эти ключевые слова описываются в следующем параграфе:

Заметка

Обработка ошибок в Swift напоминает обработку исключений (exceptions) в других языках, с использованием ключевых слов try, catch и throw. В отличие от обработки исключений во многих языках, в том числе и в Objective-C- обработка ошибок в Swift не включает разворачивание стека вызовов, то есть процесса, который может быть дорогим в вычислительном отношении. Таким образом, производительные характеристики инструкции throw сопоставимы с характеристиками оператора return.

Передача ошибки с помощью генерирующей функции

Чтобы указать, что функция, метод или инициализатор могут генерировать ошибку, вам нужно написать ключевое слово throws в реализации функции после ее параметров. Функция, отмеченная throws называется генерирующей функцией. Если у функции установлен возвращаемый тип, то вы пишете ключевое слово throws перед стрелкой возврата (->).

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

Генерирующая функция передает ошибки, которые возникают внутри нее в область вызова этой функции.

Заметка

Только генерирующая ошибку функция может передавать ошибки. Любые ошибки, сгенерированные внутри non-throwing функции, должны быть обработаны внутри самой функции.

В приведенном ниже примере VendingMachine класс имеет vend(itemNamed:) метод, который генерирует соответствующую VendingMachineError, если запрошенный элемент недоступен, его нет в наличии, или имеет стоимость, превышающую текущий депозит:

struct Item {

var price: Int

var count: Int

}

class VendingMachine {

var inventory = [

"Candy Bar": Item(price: 12, count: 7),

"Chips": Item(price: 10, count: 4),

"Pretzels": Item(price: 7, count: 11)

]

var coinsDeposited = 0

func vend(itemNamed name: String) throws {

guard let item = inventory[name] else {

throw VendingMachineError.invalidSelection

}

guard item.count > 0 else {

throw VendingMachineError.outOfStock

}

guard item.price <= coinsDeposited else {

throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)

}

coinsDeposited -= item.price

var newItem = item

newItem.count -= 1

inventory[name] = newItem

print("Dispensing \(name)")

}

}

Реализация vend(itemNamed:) метода использует оператор guard для раннего выхода из метода и генерации соответствующих ошибок, если какое-либо требование для приобретения закуски не будет выполнены. Потому что инструкция throw мгновенно изменяет управление программой, и выбранная позиция будет куплена, только если все эти требования будут выполнены.

Поскольку vend(itemNamed:) метод передает все ошибки, которые он генерирует, вызывающему его коду, то они должны быть обработаны напрямую, используя оператор do-catch, try? или try!, или должны быть переданы дальше. Например, buyFavoriteSnack(person:vendingMachine:) в примере ниже- это тоже генерирующая функция, и любые ошибки, которые генерирует метод vend(itemNamed:), будут переноситься до точки, где будет вызываться функция buyFavoriteSnack(person:vendingMachine:).

let favoriteSnacks = [

"Alice": "Chips",

"Bob": "Licorice",

"Eve": "Pretzels",

]

func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {

let snackName = favoriteSnacks[person] ?? "Candy Bar"

try vendingMachine.vend(itemNamed: snackName)

}

В этом примере, функция buyFavoriteSnack(person:vendingMachine:) подбирает любимые закуски данного человека и пытается их купить, вызывая vend(itemNamed:) метод. Поскольку метод vend(itemNamed:) может сгенерировать ошибку, он вызывается с ключевым словом try перед ним.

Генерирующие ошибку инициализаторы могут распространять ошибки таким же образом, как генерирующие ошибку функции. Например, инициализатор структуры PurchasedSnack в списке ниже вызывает генерирующую ошибку функции как часть процесса инициализации, и он обрабатывает любые ошибки, с которыми сталкивается, путем распространения их до вызывающего его объекта.

struct PurchasedSnack {

let name: String

init(name: String, vendingMachine: VendingMachine) throws {

try vendingMachine.vend(itemNamed: name)

self.name = name

}

}

Обработка ошибок с использование do-catch

Используйте инструкцию do-catch для обработки ошибок, запуская блок кода. Если выдается ошибка в коде условия do, она соотносится с условием catch для определения того, кто именно сможет обработать ошибку.

Вот общий вид условия do-catch:

do {

try выражение

выражение

} catch шаблон 1 {

выражение

} catch шаблон 2 where условие{

выражение

}

Вы пишете шаблон после ключевого слова catch, чтобы указать какие ошибки могут обрабатываться данным пунктом этого обработчика. Если условие catch не имеет своего шаблона, то оно подходит под любые ошибки и связывает ошибки к локальной константе error. Более подробно о соответствии шаблону см. Шаблоны.

Например, следующий код обрабатывает все три случая в перечислении VendingMachineError, но все другие ошибки должны быть обработаны окружающей областью:

var vendingMachine = VendingMachine()

vendingMachine.coinsDeposited = 8

do {

try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)

} catch VendingMachineError.invalidSelection {

print("Ошибка выбора.")

} catch VendingMachineError.outOfStock {

print("Нет в наличии.")

} catch VendingMachineError.insufficientFunds(let coinsNeeded) {

print("Недостаточно средств. Пожалуйста вставьте еще \(coinsNeeded) монетки.")

}

// Выведет "Недостаточно средств. Пожалуйста вставьте еще 2 монетки.

В приведенном выше примере, buyFavoriteSnack(person:vendingMachine:) функция вызывается в выражении try, потому что она может сгенерировать ошибку. Если генерируется ошибка, выполнение немедленно переносится в условия catch, которые принимают решение о продолжении передачи ошибки. Если ошибка не генерируется, остальные операторы do выполняются.

В условии catch не нужно обрабатывать все возможные ошибки, которые может вызвать код в условии do. Если ни одно из условий catch не обрабатывает ошибку, ошибка распространяется на окружающую область. Однако распространяемая ошибка должна обрабатываться некоторой внешней областью. В функции nonthrowing условие включения do-catch должно обрабатывать ошибку. В функции throwing либо включающая условие do-catch, либо вызывающая сторона должна обрабатывать ошибку. Если ошибка распространяется на область верхнего уровня без обработки, вы получите ошибку времени выполнения.

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

func nourish(with item: String) throws {

do {

try vendingMachine.vend(itemNamed: item)

} catch is VendingMachineError {

print("Invalid selection, out of stock, or not enough money.")

}

}

do {

try nourish(with: "Beet-Flavored Chips")

} catch {

print("Unexpected non-vending-machine-related error: \(error)")

}

// Выведет "Invalid selection, out of stock, or not enough money."

В nourish(with:), если vend(itemNamed выдает ошибку, которая является одним из кейсов перечисления VendingMachineError, nourish(with:) обрабатывает ошибку, печатая сообщение. В противном случае, nourish(with:) распространяет ошибку на свое место вызова. Ошибка затем попадает в общее условие catch.

Преобразование ошибок в опциональные значения

Вы можете использовать try? для обработки ошибки, преобразовав ее в опциональное значение. Если ошибка генерируется при условии try?, то значение выражения вычисляется как nil. Например, в следующем коде x и y имеют одинаковые значения и поведение:

func someThrowingFunction() throws -> Int {

// ...

}

let x = try? someThrowingFunction()

let y: Int?

do {

y = try someThrowingFunction()

} catch {

y = nil

}

Если someThrowingFunction() генерирует ошибку, значение x и y равно nil. В противном случае значение x и y - это возвращаемое значение функции. Обратите внимание, что x и y являются опциональными, независимо от того какой тип возвращает функция someThrowingFunction().

Использование try? позволяет написать краткий код обработки ошибок, если вы хотите обрабатывать все ошибки таким же образом. Например, следующий код использует несколько попыток для извлечения данных или возвращает nil, если попытки неудачные.

func fetchData() -> Data? {

if let data = try? fetchDataFromDisk() { return data }

if let data = try? fetchDataFromServer() { return data }

return nil

}

Запрет на передачу ошибок

Иногда вы знаете, что функции throw или методы не сгенерируют ошибку во время исполнения. В этих случаях, вы можете написать try! перед выражением для запрета передачи ошибки и завернуть вызов в утверждение того, что ошибка точно не будет сгенерирована. Если ошибка на самом деле сгенерирована, вы получите сообщение об ошибке исполнения.

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

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

Установка действий по очистке (Cleanup)

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

Оператор defer откладывает выполнение, пока не происходит выход из текущей области. Этот оператор состоит из ключевого слова defer и выражений, которые должны быть выполнены позже. Отложенные выражения могут не содержать кода, изменяющего контроль исполнения изнутри наружу, при помощи таких операторов как break или return, или просто генерирующего ошибку. Отложенные действия выполняются в обратном порядке, как они указаны, то есть, код в первом операторе defer выполняется после кода второго, и так далее.

func processFile(filename: String) throws {

if exists(filename) {

let file = open(filename)

defer {

close(file)

}

while let line = try file.readline() {

// работаем с файлом.

}

// close(file) вызывается здесь, в конце зоны видимости.

}

}

Приведенный выше пример использует оператор defer, чтобы удостовериться, что функция open(_:) имеет соответствующий вызов и для close(_:).

Заметка

Вы можете использовать оператор defer, даже если не используете кода обработки ошибок.

Приведение типов

Приведение типов - это способ проверить тип экземпляра и/или способ обращения к экземпляру так, как если бы он был экземпляром суперкласса или подкласса откуда-либо из своей собственной классовой иерархии.

Приведение типов в Swift реализуется с помощью операторов is и as. Эти два оператора предоставляют простой и выразительный способ проверки типа значения или преобразование значения к другому типу.

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

Определение классовой иерархии для приведения типов

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

Первый фрагмент кода определяет новый базовый класс MediaItem, Этот класс предоставляет базовую функциональность для любого вида элемента, который появляется в цифровой медиа библиотеке. А именно, он определяет свойство name типа String и инициализатор init name. (В этом примере считается, что все фильмы, песни или другие медиа штуковины имеют свойство name или попросту имя.)

class MediaItem {

var name: String

init(name: String) {

self.name = name

}

}

Следующий фрагмент определяет два подкласса класса MediaItem. Первый подкласс - Movie, он инкапсулирует дополнительную информацию о фильмах. Он добавляет свойство director поверх базового класса MediaItem с соответствующим инициализатором. Второй подкласс - Song. Этот подкласс добавляет свойство artist и инициализатор поверх базового класса:

class Movie: MediaItem {

var director: String

init(name: String, director: String) {

self.director = director

super.init(name: name)

}

}

class Song: MediaItem {

var artist: String

init(name: String, artist: String) {

self.artist = artist

super.init(name: name)

}

}

Последний отрывок кода создает неизменяемый массив library, который содержит два экземпляра Movie и три экземпляра Song. Тип library выведен во время инициализации массива литералом массива. Механизм проверки типов Swift делает вывод, что Movie, Song имеют общий суперкласс MediaItem, так что тип массива library становится [MediaItem]:

let library = [

Movie(name: "Casablanca", director: "Michael Curtiz"),

Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),

Movie(name: "Citizen Kane", director: "Orson Welles"),

Song(name: "The One And Only", artist: "Chesney Hawkes"),

Song(name: "Never Gonna Give You Up", artist: "Rick Astley")

]

// тип "library" выведен как [MediaItem]

Элементы, которые хранятся в library все еще экземпляры Movie и Song на самом деле. Однако, если вы переберете элементы массива, то они все будут одного типа MediaItem, а не Movie или Song. Для того чтобы работать с ними как с исходными типами, вам нужно проверить их типы или привести к другому типу, как указано далее.

Проверка типа

Используйте оператор проверки типа (is) для проверки того, соответствует ли тип экземпляра типам какого-то определенного подкласса. Оператор проверки типа возвращает true, если экземпляр имеет тип конкретного подкласса, false, если нет.

Пример ниже определяет две переменные movieCount и songCount, которые считают число экземпляров Movie и экземпляров Song в массиве library:

var movieCount = 0

var songCount = 0

for item in library {

if item is Movie {

movieCount += 1

} else if item is Song {

songCount += 1

}

}

print("В Media библиотеке содержится \(movieCount) фильма и \(songCount) песни")

// Выведет "В Media библиотеке содержится 2 фильма и 3 песни"

Пример перебирает все элементы массива library. На каждую итерацию цикла for-in константа item инициализируется следующим значением типа MediaItem из массива library.

Выражение item is Movie возвращает true, в том случае, если текущий item типа MediaItem является экземпляром Movie, и это выражение возвращает false, если не является экземпляром Movie. Аналогично item is Song проверяет является ли item экземпляром Song. В конце цикла for-in значения movieCount и songCount содержат количество экземпляров MediaItem каждого типа.

Понижающее приведение

Константа или переменная определенного класса может фактически ссылаться на экземпляр подкласса. Там, где вы считаете, что это тот случай, вы можете попробовать привести тип к типу подкласса при помощи оператора понижающего приведения (as? или as!).

Из-за того, что понижающее приведение может провалиться, оператор приведения имеет две формы. Опциональная форма (as?), которая возвращает опциональное значение типа, к которому вы пытаетесь привести. И принудительная форма (as!), которая принимает попытки понижающего приведения и принудительного разворачивания результата в рамках одного составного действия.

Используйте опциональную форму оператора понижающего приведения (as?), когда вы не уверены, что ваше понижающее приведение выполнится успешно. В этой форме оператор всегда будет возвращать опциональное значение, и значение будет nil, если понижающее приведение будет не выполнимо. Так же это позволяет вам проверить успешность понижающего приведения типа.

Используйте принудительную форму оператора понижающего приведения (as!), но только в тех случаях, когда вы точно уверены, что понижающее приведение будет выполнено успешно. Эта форма оператора вызовет ошибку исполнения, если вы попытаетесь таким образом привести к некорректному типу класса.

Пример ниже перебирает элементы MediaItem в массиве library и выводит соответствующее описание для каждого элемента. Чтобы сделать это, ему нужно получить доступ к каждому элементу как Movie или Song, а не просто как к MediaItem. Это необходимо для того, чтобы был доступ к свойствам director, artist, которые пригодятся нам в описании.

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

for item in library {

if let movie = item as? Movie {

print("Movie: \(movie.name), dir. \(movie.director)")

} else if let song = item as? Song {

print("Song: \(song.name), by \(song.artist)")

}

}

// Movie: Casablanca, dir. Michael Curtiz

// Song: Blue Suede Shoes, by Elvis Presley

// Movie: Citizen Kane, dir. Orson Welles

// Song: The One And Only, by Chesney Hawkes

// Song: Never Gonna Give You Up, by Rick Astley

Пример начинается с попытки понижающего приведения текущего элемента item в качестве Movie. Так как item является экземпляром MediaItem, то этот элемент может быть Movie, но он и так же может быть и Song или даже просто базовым MediaItem. Из-за этой неопределенности мы и используем опциональную форму оператора (as?). Результат выражения item as? Movie является тип Movie? или “опциональный Movie”.

Понижающее приведение к Movie проваливается, когда оно применимо к экземплярам Song в массиве library. Объединив это все, мы получаем, что пример выше использует опциональную привязку для проверки наличия значения у опционального Movie (то есть, чтобы выяснить успешность проведенной операции). Опциональная привязка записана выражением вида “if let movie = item as? Movie”, что может быть прочитано так:

“Пробуем получить доступ к item в качестве Movie. Если доступ успешен, то присвоим временную константу movie значение, которое мы получили из опционального Movie.”

Если понижающее приведение прошло успешно, то свойства movie используются в качестве вывода описания для экземпляра Movie, включая имя director. Аналогичный принцип используется при проверке экземпляров Song и при выводе описания (включая имя artist), как только находится элемент Song в массиве library.

Заметка

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

Приведение типов для Any и AnyObject

Swift предлагает два типа псевдонима типа для работы с неопределенными типами:

Any может отобразить экземпляр любого типа, включая функциональные типы.

AnyObject может отобразить экземпляр любого типа класса.

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

Вот пример использования Any с различными типами, в том числе функциональными типами и неклассовыми типами. В этом примере создается массив с именем things, который может хранить значения типа Any:

var things = [Any]()

things.append(0)

things.append(0.0)

things.append(42)

things.append(3.14159)

things.append("hello")

things.append((3.0, 5.0))

things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))

things.append({ (name: String) -> String in "Hello, \(name)" })

Массив things содержит два значения типа Int, два значения типа Double, значение типа String, кортеж типа (Double, Double), кино “Ghostbusters” и выражение замыкания, которое принимает значение String и возвращает другое значение String.

Вы можете использовать операторы is и as в кейсах конструкции switch для определения типа константы или переменной, когда известно только то, что она принадлежит типу Any или AnyObject. Пример ниже перебирает элементы в массиве things и запрашивает тип у каждого элемента с помощью конструкции switch. Несколько случаев конструкции switch привязывают их совпавшие значения к константе определенного типа, для того, чтобы потом можно было вывести значение на экран:

for thing in things {

switch thing {

case 0 as Int:

print("zero as an Int")

case 0 as Double:

print("zero as a Double")

case let someInt as Int:

print("an integer value of \(someInt)")

case let someDouble as Double where someDouble > 0:

print("a positive double value of \(someDouble)")

case is Double:

print("some other double value that I don't want to print")

case let someString as String:

print("a string value of \"\(someString)\"")

case let (x, y) as (Double, Double):

print("an (x, y) point at \(x), \(y)")

case let movie as Movie:

print("a movie called \(movie.name), dir. \(movie.director)")

case let stringConverter as (String) -> String:

print(stringConverter("Michael"))

default:

print("something else")

}

}

// zero as an Int

// zero as a Double

// an integer value of 42

// a positive double value of 3.14159

// a string value of "hello"

// an (x, y) point at 3.0, 5.0

// a movie called Ghostbusters, dir. Ivan Reitman

// Hello, Michael

Заметка

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

let optionalNumber: Int? = 3

things.append(optionalNumber) // Warning

things.append(optionalNumber as Any) // No warning

Вложенные типы

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

Чтобы вложить тип в другой тип, вам нужно написать свое определение во внешних фигурных скобках типа, который он поддерживает. Типы могут быть вложены на столько уровней, на сколько это необходимо.

Вложенные типы в действии

Пример ниже определяет структуру BlackjackCard, которая создает модель игральных карт игры Blackjack. Структура BlackjackCard содержит два вложенных перечисления типов Suit, Rank.

В игре Blackjack карта Ace (Туз) имеет значение либо один, либо одиннадцать. Это особенность отображается структурой Values, которая вложена внутрь перечисления Rank:

struct BlackjackCard {

// nested Suit enumeration

enum Suit: Character {

case spades = "♠", hearts = "♡", diamonds = "♢", clubs = "♣"

}

// nested Rank enumeration

enum Rank: Int {

case two = 2, three, four, five, six, seven, eight, nine, ten

case jack, queen, king, ace

struct Values {

let first: Int, second: Int?

}

var values: Values {

switch self {

case .ace:

return Values(first: 1, second: 11)

case .jack, .queen, .king:

return Values(first: 10, second: nil)

default:

return Values(first: self.rawValue, second: nil)

}

}

}

// BlackjackCard properties and methods

let rank: Rank, suit: Suit

var description: String {

var output = "suit is \(suit.rawValue),"

output += " value is \(rank.values.first)"

if let second = rank.values.second {

output += " or \(second)"

}

return output

}

}

Перечисления Suit описывают четыре масти при помощи символа типа Character для их отображения.

Перечисление Rank вместе со значением Int описывает тринадцать возможных рангов карт, для отображения их номинальной стоимости. (Значение Int не используется для карт Jack, Queen, King, Ace или по-русски Валета, Дамы, Короля и Туза).

Как уже упоминалось ранее, перечисление Rank определяет вложенную внутри себя структуру Values. Эта структура инкапсулирует факт того, что большинство карт имеют одно значение, но Туз имеет два значения своей карты. Структура Values определяет два свойства для того, чтобы отобразить это.

first типа Int

second типа Int? или “опциональный Int”

Rank так же определяет вычисляемое свойство values, которое возвращает экземпляр структуры Values. Это вычисляемое свойство учитывает ранг карты и инициализирует новый экземпляр Values с соответствующими значениями, основываясь на своем ранге. Оно использует специальные значения для карт Jack, Queen, King, Ace. Для карт с цифрами, оно использует значение ранга типа Int.

Сама структура BlackjackCard имеет два свойства rank, suit. Она так же определяет вычисляемое свойство description, которое использует значение, которое хранится в rank и suit, для того, чтобы создать описание имени и значения карты. Свойство description использует опциональную привязку для проверки наличия второго значения, и если оно есть, то вставляет дополнительное описание для второго значения.

Из-за того что BlackjackCard является структурой без пользовательских инициализаторов, она имеет неявный почленный инициализатор, что описано в главе “Почленные инициализаторы структурных типов”. Вы можете использовать этот инициализатор для инициализации новой константы theAceOfSpades:

let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)

print("theAceOfSpades: \(theAceOfSpades.description)")

// Выведет "theAceOfSpades: suit is ♠, value is 1 or 11"

Даже если Rank и Suit являются вложенными в BlackjackCard, их типы могут наследоваться из контекста, таким образом инициализация этого экземпляра может сослаться на члены перечисления по их именам (.ace и .spades). В примере выше свойство description корректно отображает то, что Туз (Ace) масти пики (Spades) имеет значение либо 1, либо 11.

Ссылка на вложенные типы

Для того, чтобы использовать вложенные типы снаружи определяющего их контекста, нужно поставить префикс имени типа, внутри которого он вложен, затем его имя:

let heartsSymbol = BlackjackCard.Suit.hearts.rawValue

// heartsSymbol равен "♡"

Приведенный выше пример позволяет именам Suit, Rank и Values быть намеренно короткими, потому что их имена изначально подобраны под контекст, в котором они определены.

Расширения

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

Расширения в Swift могут:

Добавлять вычисляемые свойства и вычисляемые свойства типа

Определять методы экземпляра и методы типа

Предоставлять новые инициализаторы

Определять сабскрипты (индексы)

Определять новые вложенные типы

Обеспечить соответствие существующего типа протоколу

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

Заметка

Расширения могут добавлять новую функциональность типу, но они не могут переписать существующую функциональность.

Синтаксис расширений

Расширение объявляется с помощью ключевого слова extension:

extension SomeType {

// описываем новую функциональность для типа SomeType

}

Расширение может расширить существующий тип для того, чтобы он соответствовал одному или более протоколам. Там где это имеет место, имена протоколов записываются точно так же, как и в случае с классами или структурами:

extension SomeType: SomeProtocol, AnotherProtocol {

// реализация требования протокола тут

}

Описание добавления соответствия протоколу таким образом описано в разделе Добавление реализации протокола через расширение.

Расширения могут быть использованы для расширения существующего универсального типа, что описано в Расширяем универсальный тип. Так же вы можете расширить универсальный тип, добавляя функциональность зависящую от условия, что описано в Оговорка where.

Заметка

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

Вычисляемые свойства в расширениях

Расширения могут добавлять вычисляемые свойства экземпляра и вычисляемые свойства типа к существующим типам. В примере мы добавляем пять вычисляемых свойств экземпляра во встроенный тип Double языка Swift, для обеспечения работы данного типа с единицами длины:

extension Double {

var km: Double { return self * 1_000.0 }

var m: Double { return self }

var cm: Double { return self / 100.0 }

var mm: Double { return self / 1_000.0 }

var ft: Double { return self / 3.28084 }

}

let oneInch = 25.4.mm

print("Один дюйм - это \(oneInch) метра")

// Выведет "Один дюйм- это 0.0254 метра"

let threeFeet = 3.ft

print("Три фута - это \(threeFeet) метра")

// Выведет "Три фута - это 0.914399970739201 метра"

Эти вычисляемые свойства объясняют, что тип Double должен считаться как конкретная единица измерения длины. Хотя они реализованы как вычисляемые свойства, имена этих свойств могут быть добавлены к литералу чисел с плавающей точкой через точечный синтаксис, как способ использовать значения литерала для проведения преобразований длины.

В этом примере, значение 1.0 типа Double отображает “один метр”. Это причина, по которой m возвращает self, что равно 1.m, то есть посчитать Double от числа 1.0.

Другие единицы требуют некоторых преобразований, чтобы выражать свое значение через метры. Один километр то же самое что и 1000 метров, так что km - вычисляемое свойство, которое умножает значение на 1_000.00, чтобы отобразить величину в метрах. По аналогии поступаем и с остальными свойствами, как например, с футом, футов в одном метре насчитывается 3.28024, так что вычисляемое свойство ft делит подчеркнутое значение Double на 3.28024 для того, чтобы перевести футы в метры.

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

let aMarathon = 42.km + 195.m

print("Марафон имеет длину \(aMarathon) метров")

// Выведет "Марафон имеет длину 42195.0 метров"

Заметка

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

Инициализаторы в расширениях

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

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

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

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

Пример ниже определяет структуру Rect для отображения геометрического прямоугольника. Пример так же определяет две вспомогательные структуры Size и Point, обе из которых предоставляют значения по умолчанию 0.0 для всех своих свойств:

struct Size {

var width = 0.0, height = 0.0

}

struct Point {

var x = 0.0, y = 0.0

}

struct Rect {

var origin = Point()

var size = Size()

}

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

let defaultRect = Rect()

let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),

size: Size(width: 5.0, height: 5.0))

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

extension Rect {

init(center: Point, size: Size) {

let originX = center.x - (size.width / 2)

let originY = center.y - (size.height / 2)

self.init(origin: Point(x: originX, y: originY), size: size)

}

}

Этот новый инициализатор начинается с вычисления исходной точки, основываясь на значениях свойств center и size. Потом инициализатор вызывает почленный инициализатор структуры init(origin:size:), который хранит новую исходную точку и размеры в соответствующих свойствах:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),

size: Size(width: 3.0, height: 3.0))

// исходная точка centerRect (2.5, 2.5) и его размер (3.0, 3.0)

Заметка

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

Методы в расширениях

Расширения могут добавить новые методы экземпляра или методы типа к уже существующим типам. Следующий пример добавляет новый метод экземпляра repetitions к типу Int:

extension Int {

func repetitions(task: () -> Void) {

for _ in 0..<self {

task()

}

}

}

Метод repetitions(task:) принимает единственный аргумент типа () -> Void, который указывает на функцию, которая не принимает ни одного параметра и которая не возвращает значения.

После определения расширения вы можете вызвать метод repetitions(task:) на любом целом числе, чтобы выполнить определенное задание целое число раз:

3.repetitions {

print("Hello!")

}

// Hello!

// Hello!

// Hello!

Изменяющиеся методы экземпляра

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

Пример ниже добавляет новый изменяющийся (mutating) метод square для типа Int, который возводит в квадрат исходное значение:

extension Int {

mutating func square() {

self = self * self

}

}

var someInt = 3

someInt.square()

// теперь переменная someInt имеет значение 9

Сабскрипты в расширениях

Расширения могут добавить новые сабскрипты к существующему типу. Этот пример добавляет сабскрипт целого числа во встроенный тип Int языка Swift. Этот сабскрипт [n] возвращает цифру, которая стоит на n позиции справа:

123456789[0] возвращает 9

123456789[1] возвращает 8

и так далее:

extension Int {

subscript(digitIndex: Int) -> Int {

var decimalBase = 1

for _ in 0..<digitIndex {

decimalBase *= 10

}

return (self / decimalBase) % 10

}

}

746381295[0]

// возвращает 5

746381295[1]

// возвращает 9

746381295[2]

// возвращает 2

746381295[8]

// возвращает 7

Если значение Int не имеет достаточно количество цифр для требуемого индекса, то сабскрипт возвращает 0, как если бы вместо этого числа стоял 0:

746381295[9]

// возвращает 0, как если бы вы запросили вот так:

0746381295[9]

Вложенные типы в расширениях

Расширения могут добавлять новые вложенные типы к существующим классам, структурам и перечислениям:

extension Int {

enum Kind {

case negative, zero, positive

}

var kind: Kind {

switch self {

case 0:

return .zero

case let x where x > 0:

return .positive

default:

return .negative

}

}

}

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

Так же этот пример добавляет новое вычисляемое свойство kind к типу Int, которое возвращает соответствующий член перечисления Kind для этого числа.

Вложенное перечисление может быть использовано типом Int:

func printIntegerKinds(_ numbers: [Int]) {

for number in numbers {

switch number.kind {

case .negative:

print("- ", terminator: "")

case .zero:

print("0 ", terminator: "")

case .positive:

print("+ ", terminator: "")

}

}

print("")

}

printIntegerKinds([3, 19, -27, 0, -6, 0, 7])

// Выведет "+ + - 0 - 0 + "

Эта функция printIntegerKinds(_:) принимает параметр в виде массива значений Int, затем перебирает по очереди все эти значения. Для каждого целого числа в массиве, функция смотрит на его вычисляемое свойство kind и выводит соответствующее описание.

Заметка

Как нам уже известно что number.kind имеет тип Int.Kind. Значит все значения членов Int.Kind могут быть записаны в короткой форме внутри конструкции switch, как .negative, а не Int.Kind.negative.

Протоколы

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

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

Синтаксис протокола

Определение протокола очень похоже на то, как вы определяете классы, структуры и перечисления:

protocol SomeProtocol {

// определение протокола…

}

Пользовательские типы утверждают, что они принимают протокол, когда они помещают имя протокола после имени типа и разделяются с этим именем двоеточием, то есть указывают эти протоколы как часть их определения. После двоеточия вы можете указывать множество протоколов, перечисляя их имена через запятую:

struct SomeStructure: FirstProtocol, AnotherProtocol {

// определение структуры…

}

Если у класса есть суперкласс, то вписывайте имя суперкласса до списка протоколов, которые он принимает, также разделите имя суперкласса и имя протокола запятой:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {

// определение класса…

}

Требование свойств

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

Если протокол требует от свойства быть доступным и устанавливаемым, то это требование не может полностью быть удовлетворено константой или вычисляемым свойством только для чтения (read only). Если протокол только требует от свойства читаемости (get), то такое требование может быть удовлетворено любым свойством, и это так же справедливо для устанавливаемого свойства, если это необходимо в вашем коде.

Требуемые свойства всегда объявляются как переменные свойства, с префиксом var. Свойства, значения которых вы можете получить или изменить маркируются { get set } после объявления типа свойства, а свойства, значения которых мы можем только получить, но не изменить { get }.

protocol SomeProtocol {

var mustBeSettable: Int { get set }

var doesNotNeedToBeSettable: Int { get }

}

Перед требуемыми свойствами типов пишите префикс static, когда вы определяете их в протоколе. Это правило распространяется даже тогда, когда требование свойств может иметь как префикс static так и префикс class, когда мы реализуем их в классах:

protocol AnotherProtocol {

static var someTypeProperty: Int { get set }

}

Пример протокола с единственным требуемым свойством экземпляра:

protocol FullyNamed {

var fullName: String { get }

}

Протокол FullyNamed требует у соответствующего ему типа предоставить полное имя. Протокол больше не уточняет ничего, кроме того, что тип этого свойства должен быть в состоянии предоставить свое полное имя. Протокол утверждает, что любой тип FullyNamed должен иметь свойство fullName, значение которого может быть получено, и это значение должно быть типа String.

Ниже приведен пример структуры, которая принимает и полностью соответствует протоколу FullyNamed:

struct Person: FullyNamed {

var fullName: String

}

let john = Person(fullName: "John Appleseed")

// john.fullName равен "John Appleseed"

Этот пример определяет структуру Person, которая отображает персону с конкретным именем. Она утверждает, что она принимает протокол FullyNamed в качестве первой строки собственного определения.

Каждый экземпляр Person имеет единственное свойство fullName типа String. Это удовлетворяет единственному требованию протокола FullyNamed, и это значит, что Person корректно соответствует протоколу. (Swift сообщает об ошибке во время компиляции, если требования протокола выполняются не полностью.)

Ниже представлен более сложный класс, который так же принимает и соответствует протоколу FullyNamed:

class Starship: FullyNamed {

var prefix: String?

var name: String

init(name: String, prefix: String? = nil) {

self.name = name

self.prefix = prefix

}

var fullName: String {

return (prefix != nil ? prefix! + " " : "") + name

}

}

var ncc1701 = Starship(name: "Enterprise", prefix: "USS")

// ncc1701.fullName равен "USS Enterprise"

Класс реализует требуемое свойство fullName, в качестве вычисляемого свойства только для чтения (для космического корабля). Каждый экземпляр класса Starship хранит обязательный name и опциональный prefix. Свойство fullName использует значение prefix, если оно существует и устанавливает его в начало name, чтобы получилось целое имя для космического корабля.

Требование методов

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

Как и в случае с требованиями свойств типа, вы всегда указываете префикс static, когда они определяются в протоколе. И это верно даже тогда, когда требования к методу типа имеет префикс static или class, когда реализуется классом:

protocol SomeProtocol {

static func someTypeMethod()

}

Следующий пример определяет протокол с единственным требуемым методом экземпляра:

protocol RandomNumberGenerator {

func random() -> Double

}

Этот протокол RandomNumberGenerator требует любой соответствующий ему тип иметь метод экземпляра random, который при вызове возвращает значение типа Double. Хотя это и не указано как часть протокола, но предполагается, что значение будет числом от 0.0 и до 1.0 (не включительно).

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

Ниже приведена реализация класса, который принимает и соответствует протоколу RandomNumberGenerator. Этот класс реализует алгоритм генератора псевдослучайных чисел, известный как алгоритм линейного конгруэнтного генератора:

class LinearCongruentialGenerator: RandomNumberGenerator {

var lastRandom = 42.0

let m = 139968.0

let a = 3877.0

let c = 29573.0

func random() -> Double {

lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))

return lastRandom / m

}

}

let generator = LinearCongruentialGenerator()

print("Here's a random number: \(generator.random())")

// Выведет "Случайное число: 0.37464991998171"

print("And another one: \(generator.random())")

// Выведет "Другое случайное число: 0.729023776863283"

Требования изменяющих методов

Иногда необходимо для метода изменить (или мутировать) экземпляр, которому он принадлежит. Для методов экземпляра типа значения (структура, перечисление) вы располагаете ключевое слово mutating до слова метода func, для индикации того, что этому методу разрешено менять экземпляр, которому он принадлежит, и/или любое свойство этого экземпляра. Этот процесс описан в главе Изменение типов значений методами экземпляра.

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

Заметка

Если вы поставили ключевое слово mutating перед методом требуемым протоколом экземпляра, то вам не нужно писать слово mutating при реализации этого метода для класса. Слово mutating используется только структурами или перечислениями.

Пример ниже определяет протокол Togglable, который определяет единственный требуемый метод экземпляра toggle(). Как и предполагает имя метода, он переключает или инвертирует состояние любого типа, обычно меняя свойство этого типа.

Метод toggle() имеет слово mutating как часть определения протокола Togglable, для отображения того, что этот метод меняет состояние соответствующего протоколу экземпляра при своем вызове:

protocol Togglable {

mutating func toggle()

}

Если вы реализуете протокол Togglable для структур или перечислений, то эта структура или перечисление может соответствовать протоколу предоставляя реализацию метода toggle(), который так же будет отмечен словом mutating.

Пример ниже определяет перечисление OnOffSwitch. Это перечисление переключается между двумя состояниями, отмеченными двумя случаями перечислениям .on и .off. Реализация метода toggle перечисления отмечена словом mutating, чтобы соответствовать требованию протокола:

enum OnOffSwitch: Togglable {

case off, on

mutating func toggle() {

switch self {

case .off:

self = .on

case .on:

self = .off

}

}

}

var lightSwitch = OnOffSwitch.off

lightSwitch.toggle()

// lightSwitch теперь равен .on

Требование инициализатора

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

protocol SomeProtocol {

init(someParameter: Int)

}

Реализация класса соответствующему протоколу с требованием инициализатора

Вы можете реализовать требуемый инициализатор в классе, соответствующем протоколу, в качестве назначенного инициализатора или вспомогательного. В любом случае вам нужно отметить этот инициализатор ключевым словом required:

class SomeClass: SomeProtocol {

required init(someParameter: Int) {

// реализация инициализатора…

}

}

Использование модификатора required гарантирует, что вы проведете явную или унаследованную реализацию требуемого инициализатора на всех подклассах соответствующего класса протоколу, так, чтобы они тоже соответствовали протоколу. Подробнее вы можете прочитать в главе Требуемые инициализаторы.

Заметка

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

Если подкласс переопределяет назначенный инициализатор суперкласса и так же реализует соответствующий инициализатор протоколу, то обозначьте реализацию инициализатора сразу двумя модификаторами required и override:

protocol SomeProtocol {

init()

}

class SomeSuperClass {

init() {

// реализация инициализатора…

}

}

class SomeSubClass: SomeSuperClass, SomeProtocol {

// "required" от соответсвия протоколу SomeProtocol; "override" от суперкласса SomeSuperClass

required override init() {

// реализация инициализатора…

}

}

Требуемые проваливающиеся инициализаторы

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

Требование проваливающегося инициализатора может быть удовлетворено проваливающимся инициализатором или непроваливающимся инициализатором соответствующего протоколу типа. Требование непроваливающегося инициализатора может быть удовлетворено непроваливающимся инициализатором или неявно развернутым проваливающимся инициализатором.

Протоколы как типы

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

Так как это тип, то вы можете использовать протокол во многих местах, где можно использовать другие типы:

Как тип параметра или возвращаемый тип в функции, методе, инициализаторе

Как тип константы, переменной или свойства

Как тип элементов массива, словаря или другого контейнера

Заметка

Из-за того что протоколы являются типами, то их имена начинаются с заглавной буквы (как в случае FullyNamed или RandomNumberGenerator) для соответствия имен с другими типами Swift (Int, String, Bool, Double…)

Вот пример использования протокола в качестве типа:

class Dice {

let sides: Int

let generator: RandomNumberGenerator

init(sides: Int, generator: RandomNumberGenerator) {

self.sides = sides

self.generator = generator

}

func roll() -> Int {

return Int(generator.random() * Double(sides)) + 1

}

}

Этот пример определяет новый класс Dice, который отображает игральную кость с n количеством сторон для настольной игры. Экземпляры Dice имеют свойство sides, которое отображает количество сторон, которое они имеют, так же кубики имеют свойство generator, которое предоставляет генератор случайных чисел, из которого и берутся значения броска игрального кубика.

Свойство generator имеет тип RandomNumberGenerator, таким образом вы можете использовать в этом свойстве экземпляр любого типа, соответствующий протоколу RandomNumberGenerator. Больше ничего не требуется от экземпляра, присваемого этому свойству, кроме того, что этот экземпляр должен принимать протокол RandomNumberGenerator.

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

Dice предоставляет один метод экземпляра - roll, который возвращает целое значение от 1 и до количества сторон на игральной кости. Этот метод вызывает генератор метода random(), для создания нового случайного числа от 0.0 и до 1.0, а затем использует это случайное число для создания значения броска игральной кости в соответствующем диапазоне (1…n). Так как мы знаем, что generator принимает RandomNumberGenerator, то это гарантирует нам, что у нас будет метод random().

Вот как используется класс Dice для создания шестигранный игральной кости с экземпляром LinearCongruentialGenerator в качестве генератора случайных чисел:

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())

for _ in 1...5 {

print("Бросок игральной кости равен \(d6.roll())")

}

// Бросок игральной кости равен 3

// Бросок игральной кости равен 5

// Бросок игральной кости равен 4

// Бросок игральной кости равен 5

// Бросок игральной кости равен 4

Делегирование

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

Пример ниже определяет два протокола для использования в играх, основанных на бросках игральных костей:

protocol DiceGame {

var dice: Dice { get }

func play()

}

protocol DiceGameDelegate {

func gameDidStart(_ game: DiceGame)

func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)

func gameDidEnd(_ game: DiceGame)

}

Протокол DiceGame является протоколом, который может быть принят любой игрой, которая включает игральную кость.

Протокол DiceGameDelegate может быть принят любым типом для отслеживания прогресса DiceGame. Для предотвращения цикла сильных ссылок, делегаты определены как weak ссылки. Для дополнительной информации смотрите раздел Циклы сильных ссылок между экземплярами классов. Маркировка протокола "только для классов" позволяет классу SnakesAndLadders далее в этой главе объявить, что его делегат должен использовать слабую ссылку. Классовые протоколы наследуют протокол AnyObject, о чем подробнее говорится в Классовых протоколах.

Вот версия игры “Змеи и лестницы”, которая первоначально была представлена в разделе Управление потоком. Эта версия адаптирована под использование экземпляра Dice для своих бросков кости, для соответствия протоколу DiceGame и для уведомления DiceGameDelegate о прогрессе:

class SnakesAndLadders: DiceGame {

let finalSquare = 25

let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())

var square = 0

var board: [Int]

init() {

board = Array(repeating: 0, count: finalSquare + 1)

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02

board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

}

var delegate: DiceGameDelegate?

func play() {

square = 0

delegate?.gameDidStart(self)

gameLoop: while square != finalSquare {

let diceRoll = dice.roll()

delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)

switch square + diceRoll {

case finalSquare:

break gameLoop

case let newSquare where newSquare > finalSquare:

continue gameLoop

default:

square += diceRoll

square += board[square]

}

}

delegate?.gameDidEnd(self)

}

}

Для описания процесса игры “Змеи и лестницы”, посмотрите раздел инструкции Break.

Эта версия игры обернута в класс SnakesAndLadders, который принимает протокол DiceGame. Он предоставляет свойство dice и метод play() для соответствия протоколу. (Свойство dice объявлено как константное свойство, потому что оно не нуждается в изменении значения после инициализации, а протокол требует только чтобы оно было доступным.)

Настройка игры “Змеи и лестницы” происходит в инициализаторе класса init(). Вся логика игры перемещается в метод play протокола, который использует требуемое свойство протокола для предоставления значений броска игральной кости.

Обратите внимание, что свойство delegate определено как опциональное DiceGameDelegate, потому что делегат не требуется для игры. Так как оно является опциональным типом, свойство delegate автоматически устанавливает начальное значение равное nil. Таким образом, у каждого экземпляра игры есть установки свойства подходящему делегату.

DiceGameDelegate предоставляет три метода для отслеживания прогресса игры. Эти три метода были включены в логику игры внутри метода play(), и вызываются когда начинается новая игра, начинается новый ход или игра кончается.

Так как свойство delegate является опциональным DiceGameDelegate, метод play() использует опциональную последовательность каждый раз, как вызывается этот метод у делегата. Если свойство delegate равно nil, то этот вызов этого метода делегатом проваливается без возникновения ошибки. Если свойство delegate не nil, вызываются методы делегата, которые передаются в экземпляр SnakesAndLadders в качестве параметра.

Следующий пример показывает класс DiceGameTracker, который принимает протокол DiceGameDelegate:

class DiceGameTracker: DiceGameDelegate {

var numberOfTurns = 0

func gameDidStart(_ game: DiceGame) {

numberOfTurns = 0

if game is SnakesAndLadders {

print("Начали новую игру Змеи и лестницы")

}

print("У игральной кости \(game.dice.sides) граней")

}

func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {

numberOfTurns += 1

print("Выкинули \(diceRoll)")

}

func gameDidEnd(_ game: DiceGame) {

print("Длительность игры \(numberOfTurns) хода")

}

}

DiceGameTracker реализует все три метода, которые требует DiceGameDelegate. Он использует эти методы для отслеживания количества ходов, которые были сделаны в игре. Он сбрасывает значение свойства numberOfTurns на ноль, когда начинается игра и увеличивает каждый раз, как начинается новый ход и выводит общее число ходов, как только кончается игра.

Реализация gameDidStart(_:), показанная ранее, использует параметр game для отображения вступительной информации об игре, в которую будут играть. Параметр game имеет тип DiceGame, но не SnakesAndLadders, так что gameDidStart(_:) может получить и использовать только те методы и свойства, которые реализованы как часть протокола DiceGame. Однако метод все еще может использовать приведение типов для обращения к типу основного (исходного) экземпляра. В этом примере, он проверяет действительно ли game является экземпляром SnakesAndLadders или нет, а потом выводит соответствующее сообщение.

gameDidStart(_:) так же получает доступ к свойству dice, передаваемого параметра game. Так как известно, что game соответствует протоколу DiceGame, то это гарантирует наличие свойства dice, таким образом метод gameDidStart(_:) может получить доступ и вывести сообщение о свойстве кости sides, независимо от типа игры, в которую играют.

Теперь давайте взглянем на то, как выглядит DiceGameTracker в действии:

let tracker = DiceGameTracker()

let game = SnakesAndLadders()

game.delegate = tracker

game.play()

// Начали новую игру Змеи и лестницы

// У игральной кости 6 граней

// Выкинули 3

// Выкинули 5

// Выкинули 4

// Выкинули 5

// Длительность игры 4 хода

Добавление реализации протокола через расширение

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

Заметка

Существующие экземпляры типа автоматически принимают и отвечают требованиям протокола, когда опции, необходимые для соответствия добавляются через расширение типа.

К примеру, этот протокол TextRepresentable может быть реализован любым типом, который может отображать текст. Это может быть собственное описание или текстовая версия текущего состояния:

protocol TextRepresentable {

var textualDescription: String { get }

}

Класс Dice, о котором мы говорили ранее, может быть расширен для принятия и соответствия протоколу TextRepresentable:

extension Dice: TextRepresentable {

var textualDescription: String {

return "Игральная кость с \(sides) гранями"

}

}

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

Теперь экземпляр Dice может быть использован как TextRepresetable:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())

print(d12.textualDescription)

// Выведет "Игральная кость с 12 гранями"

Аналогично игровой класс SnakesAndLadders может быть расширен для того, чтобы смог принять и соответствовать протоколу TextRepresentable:

extension SnakesAndLadders: TextRepresentable {

var textualDescription: String {

return "Игра Змеи и Лестницы с полем в \(finalSquare) клеток"

}

}

print(game.textualDescription)

// Выведет "Игра Змеи и Лестницы с полем в 25 клеток"

Условное соответствие протоколу

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

Следующее расширение делает экземпляры Array совместимыми с TextRepresentable протоколом всякий раз, когда они хранят элементы типа, которые соответствуют TextRepresentable:

extension Array: TextRepresentable where Element: TextRepresentable {

var textualDescription: String {

let itemsAsText = self.map { $0.textualDescription }

return "[" + itemsAsText.joined(separator: ", ") + "]"

}

}

let myDice = [d6, d12]

print(myDice.textualDescription)

// Prints "[A 6-sided dice, A 12-sided dice]"

Принятие протокола через расширение

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

struct Hamster {

var name: String

var textualDescription -> String {

return "Хомяка назвали \(name)"

}

}

extension Hamster: TextRepresentable {}

Экземпляры Hamster теперь могут быть использованы в тех случаях, когда нужен тип TextRepresentable:

let simonTheHamster = Hamster(name: "Фруша")

let somethingTextRepresentable: TextRepresentable = simonTheHamster

print(somethingTextRepresentable.textualDescription)

// Выведет "Хомяка назвали Фруша"

Заметка

Типы не принимают протоколы автоматически, если они удовлетворяют их требованиям. Принятие протокола должно быть объявлено в явной форме.

Коллекции типов протокола

Протоколы могут использовать в качестве типов, которые хранятся в таких коллекциях как массивы или словари, что упоминалось ранее в Протоколы как типы.

Пример ниже создает массив из элементов типа TextRepresentable:

let things: [TextRepresentable] = [game, d12, simonTheHamster]

Теперь мы можем перебирать элементы массива и выводить текстовое отображение каждого из них:

for thing in things {

print(thing.textualDescription)

}

// Игра Змеи и Лестницы с полем в 25 клеток

// Игральная кость с 12 гранями

// Хомяка назвали Фруша

Обратите внимание, что константа thing является типом TextRepresentable. Она не является типом Dice, или DiceGame, или Hamster, даже в том случае, если базовый тип является одним из них. Тем не менее из-за того, что она типа TextRepresentable, а все что имеет тип TextRepresentable, имеет метод textualDescription, что значит, что можно безопасно вызывать thing.textualDescription каждую итерацию цикла.

Наследование протокола

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

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {

// определение протокола…

}

Ниже приведен пример протокола, который наследует протокол TextRepresentable, о котором мы говорили ранее:

protocol PrettyTextRepresentable: TextRepresentable {

var prettyTextualDescription: String { get }

}

Этот пример определяет новый протокол PrettyTextRepresentable, который наследует из TextRepresentable. Все, что соответствует протоколу PrettyTextRepresentable, должно удовлетворять всем требованиям TextRepresentable, плюс дополнительные требования введенные от протокола PrettyTextRepresentable. В этом примере PrettyTextRepresentable добавляет единственное требование обеспечить read-only свойство экземпляра prettyTextualDescription, которое возвращает String.

Класс SnakesAndLadders может быть расширен, чтобы иметь возможность принять и соответствовать PrettyTextRepresentable:

extension SnakesAndLadders: PrettyTextRepresentable {

var prettyTextualDescription: String {

var output = textualDescription + ":\n"

for index in 1...finalSquare {

switch board[index] {

case let ladder where ladder > 0:

output += "▲ "

case let snake where snake < 0:

output += "▼ "

default:

output += "○ "

}

}

return output

}

}

Это расширение утверждает, что оно принимает протокол PrettyTextRepresentable и реализует свойство prettyTextualDescription для типа SnakesAndLadders. Все, что является типом PrettyTextRepresentable, так же должно быть и TextRepresentable, таким образом, реализация prettyTextualDescription начинается с обращения к свойству textualDescription из протокола TextRepresentable для начала вывода строки. Затем он добавляет двоеточие и символ разрыва строки для начала текстового отображения. Затем он проводит перебор элементов массива (клеток доски) и добавляет их геометрические формы для отображения контента:

Если значение клетки больше нуля, то это является началом лестницы, и это отображается символом ▲.

Если значение клетки меньше нуля, то это голова змеи, и эта ячейка имеет символ ▼.

И наоборот, если значение клетки равно нулю, то это “свободная” клетка, которая отображается символом ○.

Эта реализация метода может быть использована для вывода текстового описания любого экземпляра SnakesAndLadders:

print(game.prettyTextualDescription)

// Игра Змеи и Лестницы с полем в 25 клеток:

// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

Классовые протоколы

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

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {

// определение протокола типа class-only

}

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

Заметка

Используйте протоколы class-only, когда поведение, определяемое протоколом, предполагает или требует, что соответствующий протоколу тип должен быть ссылочного типа, а не типом значении. Для более детального исследования с вашей стороны прочитайте главы: Структуры и перечисления - типы значения и Классы - ссылочный тип.

Композиция протоколов

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

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

Ниже приведен пример, который комбинирует два протокола Named и Aged в одно единственное требование композиции протоколов в качестве параметра функции:

protocol Named {

var name: String { get }

}

protocol Aged {

var age: Int { get }

}

struct Person: Named, Aged {

var name: String

var age: Int

}

func wishHappyBirthday(to celebrator: Named & Aged) {

print("С Днем Рождения, \(celebrator.name)! Тебе уже \(celebrator.age)!")

}

let birthdayPerson = Person(name: "Сашка", age: 21)

wishHappyBirthday(to: birthdayPerson)

// Выведет "С Днем Рождения, Сашка! Тебе уже 21!"

В этом примере мы определяем протокол Named с единственным требованием свойства name типа String, значение которого мы можем получить. Так же мы определяем протокол Aged с единственным требованием свойства age типа Int, значение которого мы так же должны мочь получить. Оба этих протокола принимаются структурой Person.

Этот пример определяет функцию wishHappyBirthday(to:), которая принимает единственный параметр celebrator. Тип этого параметра Named & Aged, что означает “любой тип, который соответствует сразу двум протоколам Aged и Named”. Это не важно какой тип передается в качестве параметра функции до тех пор, пока он соответствует этим протоколам.

Далее в примере мы создает экземпляр birthdayPerson класса Person и передаем этот новый экземпляр в функцию wishHappyBirthday(to:). Из-за того что Person соответствует двум протоколам, то функция wishHappyBirthday(to:) может вывести поздравление с днем рождения.

Следующий пример показывает как вы можете объединить протокол Named с классом Location:

class Location {

var latitude: Double

var longitude: Double

init(latitude: Double, longitude: Double) {

self.latitude = latitude

self.longitude = longitude

}

}

class City: Location, Named {

var name: String

init(name: String, latitude: Double, longitude: Double) {

self.name = name

super.init(latitude: latitude, longitude: longitude)

}

}

func beginConcert(in location: Location & Named) {

print("Hello, \(location.name)!")

}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)

beginConcert(in: seattle)

// Выведет "Hello, Seattle!"

Функция beginConcert(in:) принимает параметр типа Location и Named, что означает любой тип, который является подклассом Location, и который будет реализовывать протокол Named. В этом случае City удовлетворяем обоим требованиям.

Передавая birthdayPerson в функцию beginConcert(in:) некорректно, так как Person не является подклассом Location. И наоборот, если вы создали подкласс Location, который не реализует протокол Named, то вызов метода beginConcert(in:) с этим экземпляром так же является некорректным.

Проверка соответствия протоколу

Вы можете использовать операторы is и as, которые описаны в главе Приведение типов, для проверки соответствия протоколу и приведению к определенному протоколу. Приведение к протоколу проходит точно так же как и приведение к типу:

Оператор is возвращает значение true, если экземпляр соответствует протоколу и возвращает false, если нет.

Опциональная версия оператора понижающего приведения as? возвращает опциональное значение типа протокола, и это значение равно nil, если оно не соответствует протоколу.

Принудительная версия оператора понижающего приведения as осуществляет принудительное понижающее приведение, и если оно не завершается успешно, то выскакивает runtime ошибка.

Этот пример определяет протокол HasArea с единственным требованием свойства area типа Double (доступное свойство):

protocol HasArea {

var area: Double { get }

}

Ниже представлены два класса Circle, Country, оба из которых соответствуют протоколу HasArea:

class Circle: HasArea {

let pi = 3.1415927

var radius: Double

var area: Double { return pi * radius * radius }

init(radius: Double) { self.radius = radius }

}

class Country: HasArea {

var area: Double

init(area: Double) { self.area = area }

}

Класс Circle реализует требование свойства area в качестве вычисляемого свойства, основываясь на хранимом свойстве radius. Класс Country реализует требование area напрямую в качестве хранимого свойства. Оба класса корректно соответствуют протоколу HasArea.

Ниже приведен класс Animal, который не соответствует протоколу HasArea:

class Animal {

var legs: Int

init(legs: Int) { self.legs = legs }

}

Классы Circle, Country, Animal не имеют общего базового класса. Тем не менее они все являются классами, и их экземпляры могут быть использованы для инициализации массива, который хранит значения типов AnyObject:

let objects: [AnyObject] = [

Circle(radius: 2.0),

Country(area: 243_610),

Animal(legs: 4)

]

Массив objects инициализирован при помощи литерала, содержащего экземпляры Circle, который имеет radius равный 2, экземпляр типа Country, который инициализирован площадью Великобритании в квадратных километрах, и экземпляром класса Animal, который инициализирован количеством ног.

Массив objects может быть перебран, и каждый элемент массива может быть проверен на соответствие протоколу HasArea:

for object in objects {

if let objectWithArea = object as? HasArea {

print("Площадь равна \(objectWithArea.area)")

} else {

print("Что-то такое, что не имеет площади")

}

}

// Площадь равна 12.5663708

// Площадь равна 243610.0

// Что-то такое, что не имеет площади

Каждый раз, когда объект массива соответствует протоколу HasArea, возвращается опциональное значение при помощи оператора as?, которое разворачивается при помощи опциональной связки в константу objectWithArea. Константа objectWithArea является типом HasArea, таким образом, свойство area может быть доступно и выведено на экран способом вывода через сам тип.

Обратите внимание, что базовые объекты не меняются в процессе приведения типа. Они остаются Circle, Country и Animal. Однако в момент, когда они хранятся в константе objectWithArea, известно лишь то, что они являются типом HasArea, так что мы можем получить доступ только к свойству area.

Опциональные требования протокола

Вы можете определить опциональные требования для протокола. Эти требования не обязательно должны быть реализованы для соответствия протоколу. Опциональные требования должны иметь префиксный модификатор optional в качестве части определения протокола. Таким образом вы можете писать код, который взаимодействует с кодом на Objective-C. Имеется в виду, что без @objc код не будет компилироваться, и при этом наличие @objc позволяет коду Swift взаимодействовать с кодом Objective-C. И протокол, и опциональное требование должны иметь атрибут @objc. Обратите внимание, что протоколы с маркировкой @objc могут приниматься только классами, но не структурами или перечислениями.

Когда вы используете опциональное требование свойства или метода, то их тип автоматически становится опциональным. Например, тип метода (Int) -> String становится ((Int) -> String)?. Обратите внимание, что весь тип функции обернут в опциональное значение, а не только возвращаемое значение функции.

Опциональное требование протокола может быть вызвано при помощи опциональной цепочки, чтобы учесть возможность того, что требование не будет реализовано типом, который соответствует протоколу. Вы проверяете реализацию опционального метода, написав вопросительный знак после имени метода, когда он вызывается, например someOptionalMethod?(someArgument). Для более полной информации о опциональной последовательности читайте Опциональная последовательность.

Следующий пример определяет класс Counter, который использует источник внешних данных для предоставления значение их инкремента. Этот источник внешних данных определен протоколом CounterDataSource, который имеет два опциональных требования:

@objc protocol CounterDataSource {

@objc optional func increment(forCount count: Int) -> Int

@objc optional var fixedIncrement: Int { get }

}

Протокол CounterDataSource определяет опциональное требование метода increment(forCount:) и опциональное требование свойства fixedIncrement. Эти требования определяют два разных способа для источника данных для предоставления подходящего значения инкремента для экземпляра Counter.

Заметка

Строго говоря, вы можете написать пользовательский класс, который соответствует протоколу CounterDataSource без реализации какого-либо требования этого протокола. Они оба опциональные, в конце концов. Хотя технически это допускается, но это не будет реализовываться для хорошего источника данных.

Класс Counter, определенный ниже, имеет опциональное свойство dataSource типа CounterDataSource?:

class Counter {

var count = 0

var dataSource: CounterDataSource?

func increment() {

if let amount = dataSource?.increment?(forCount: count) {

count += amount

} else if let amount = dataSource?.fixedIncrement {

count += amount

}

}

}

Класс Counter хранит свое текущее значение в переменном свойстве count. Класс Counter так же определяет метод increment, который увеличивает свойство count, каждый раз, когда вызывается этот метод.

Метод increment сначала пытается получить значение инкремента, заглядывая в реализацию метода increment(forCount:) в его источнике данных. Метод increment использует опциональную последовательность для попытки вызвать increment(forCount:) и передает текущее значение count как единственный аргумент метода.

Обратите внимание, что здесь всего два уровня опциональной последовательности. Первый - возможный источник данных dataSource, который может быть nil, так что dataSource имеет вопросительный знак после имени для индикации того, что метод increment(forCount:) может быть вызван только в том случае, если dataSource не nil. Второй уровень говорит нам о том, что даже если dataSource существует, у нас все равно нет гарантии того, что он реализует метод increment(forCount:), потому что это опциональное требование. Есть вероятность, что increment(forCount:) так же не реализован из-за опциональной цепочки. Вызов increment(forCount:) происходит только, если increment(forCount:) существует и не равен nil.  Именно по этой причине increment(forCount:) записан с вопросительным знаком после своего имени.

Так как вызов increment(forCount:) может провалиться по одной из этих двух причин, вызов возвращает нам значение типа опционального Int. Это верно даже если increment(forCount:) определено как возвращающее неопциональное значение Int в определении CounterDataSource.  Даже если подряд идут две опциональные операции, одна сразу после другой, то результат все равно будет иметь единственный завернутый опционал. Подробнее читайте Соединение нескольких уровней опциональных последовательностей.

После вызова increment(forCount:) опциональный Int, который он возвращает, разворачивается в константу amount, при помощи опциональной связки. Если опциональный Int содержит значения, то есть, если делегат и метод существуют, и метод вернул значение, то неразвернутое значение amount прибавляется в свойство count, и на этом реализация завершается.

Если же нет возможности получить значение из метода increment(forCount:) по причине dataSource равен nil или из-за того что у источника данных нет реализации метода increment(forCount:), а следовательно вместо этого метод increment пытается получить значение от источника данных fixedIncrement. Свойство fixedIncrement является опциональным требованием, так что его имя так же написано в опциональной последовательности с вопросительным знаком, что служит индикатором того, что попытка получить значение этого свойства может привести к провалу. Как и раньше, возвращаемое значение является опциональной Int, даже тогда fixedIncrement определен как свойство типа неопционального Int, в качестве части определения протокола CounterDataSource.

Ниже приведена простая реализация CounterDataSource, где источник данных возвращает постоянное значение 3, каждый раз, как получает запрос. Это осуществляется благодаря тому, что реализуется опциональное требование свойства fixedIncrement:

class ThreeSource: NSObject, CounterDataSource {

let fixedIncrement = 3

}

Вы можете использовать экземпляр ThreeSource в качестве источника данных для новых экземпляров Counter:

var counter = Counter()

counter.dataSource = ThreeSource()

for _ in 1...4 {

counter.increment()

print(counter.count)

}

// 3

// 6

// 9

// 12

Код, приведенный выше, создает новый экземпляр Counter, устанавливает его источник данных как экземпляр ThreeSource и вызывает метод счетчика increment четыре раза. Как и ожидалось, свойство счетчика увеличивается на три каждый раз, когда вызывается increment.

Ниже приведен более сложный источник данных TowardsZeroSource, который заставляет экземпляр Counter считать в сторону увеличения или уменьшения по направлению к нулю от текущего значения count:

class TowardsZeroSource: NSObject, CounterDataSource {

func increment(forCount count: Int) -> Int {

if count == 0 {

return 0

} else if count < 0 {

return 1

} else {

return -1

}

}

}

Класс TowardsZeroSource реализует опциональный метод increment(forCount:) из протокола CounterDataSource и использует значение аргумента count для определения направления следующего счета. Если count уже ноль, то метод возвращает 0, для отображения того, что дальнейших вычислений не требуется.

Вы можете использовать экземпляр TowardsZeroSource с уже существующим экземпляром Counter для отсчета от -4 и до 0. Как только счетчик достигает 0, вычисления прекращаются:

counter.count = -4

counter.dataSource = TowardsZeroSource()

for _ in 1...5 {

counter.increment()

print(counter.count)

}

// -3

// -2

// -1

// 0

// 0

Расширения протоколов

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

Например, RandomNumberGenerator протокол может быть расширен для обеспечения randomBool() метода, который использует результат вызванного random() метода для возврата random (случайного) значения Bool:

extension RandomNumberGenerator {

func randomBool() -> Bool {

return random() > 0.5

}

}

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

let generator = LinearCongruentialGenerator()

print("Рандомное число: \(generator.random())")

// Выведет "Рандомное число: 0.37464991998171"

print("Рандомное логическое значение: \(generator.randomBool())")

// Выведет "Рандомное логическое значение: true"

Расширения протоколов могут добавлять реализацию к соответствующим типам данных, но не могут расширить протокол или унаследовать от другого протокола. Наследование протокола всегда указывается в самом объявлении протокола.

Обеспечение реализации по умолчанию (дефолтной реализации)

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

Заметка

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

Например, протокол PrettyTextRepresentable, который наследует от протокола TextRepresentable может предоставлять дефолтную реализацию требуемого свойства prettyTextualDescription, просто возвращая результат обращения к свойству textualDescription:

extension PrettyTextRepresentable {

var prettyTextualDescription: String {

return textualDescription

}

}

Добавление ограничений к расширениям протоколов

Когда вы определяете расширение протокола, вы можете указать ограничения для принимающих типов, которые они должны удовлетворить до того, как будут доступны методы и свойства расширения. Вы записываете эти ограничения сразу после имени протокола, при помощи оговорки where. Более подробно об оговорках where, читайте в разделе "Оговорка Where".

Например, вы можете определить расширение протокола Collection, которое применимо ко всем коллекциям, чьи элементы соответствуют протоколу Equatable. Ограничивая элементы Collection протоколом Equatable, частью стандартной библиотеки, вы можете использовать операторы == и != для проверки равенства и неравенства между двумя элементами.

extension Collection where Element: Equatable {

func allEqual() -> Bool {

for element in self {

if element != self.first {

return false

}

}

return true

}

}

Метод allEqual() возвращает true, только если все элементы в Collection равны.

Рассмотрим два целочисленных массива: один, где все элементы одинаковы, и другой, где элементы различны:

let equalNumbers = [100, 100, 100, 100, 100]

let differentNumbers = [100, 100, 200, 100, 200]

Поскольку массивы соответствуют Collection и целые числа соответствуют Equatable, equalNumbers и differentNumbers могут использовать метод allEqual():

print(equalNumbers.allEqual())

// Prints "true"

print(differentNumbers.allEqual())

// Prints "false"

Заметка

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

Универсальные шаблоны

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

Универсальные шаблоны одна из самых мощных особенностей Swift, и большая часть всех библиотек Swift построена на основе универсального кода. На самом деле вы используете универсальный код все время, даже если вы этого не осознаете. Например, коллекции Swift Array или Dictionary являются универсальными. Вы можете создать массив, который содержит значения типа Int или массив, который содержит значения String, или на самом деле любой другой массив, который может содержать любой другой тип. Аналогично вы создаете словарь, который может содержать значения разных типов, и нет никакого ограничения по типу хранящихся значений.

Проблема, которую решают универсальные шаблоны

Приведем обычную, стандартную, неуниверсальную функцию swapTwoInts(_:_:), которая меняет два Int местами:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {

let temporaryA = a

a = b

b = temporaryA

}

Эта функция использует сквозные параметры для замещения значения a и b, что описано в Сквозные параметры.

Функция swapTwoInts(_:_:) обменивает начальные значения переменных a и b местами. Вы можете использовать эту функцию для замещения двух значений типа Int:

var someInt = 3

var anotherInt = 107

swapTwoInts(&someInt, &anotherInt)

print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")

// Выведет "someInt is now 107, and anotherInt is now 3"

Функция swapTwoInts(_:_:) полезная, но она применима только для значений типа Int. Если вы хотите поменять местами два значения типа String или два значения Double, то вам придется написать больше функций, к примеру, swapTwoStrings(_:_:) или swapTwoDoubles(_:_:), которые показаны ниже:

func swapTwoStrings(_ a: inout String, _ b: inout String) {

let temporaryA = a

a = b

b = temporaryA

}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {

let temporaryA = a

a = b

b = temporaryA

}

Вы может быть заметили, что тела функций swapTwoInts(_:_:), swapTwoStrings(_:_:), и swapTwoDouble(_:_:) идентичны. Единственное отличие в том, что они поддерживают значения различных типов (Int, String, и Double).

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

Заметка

Во всех трех функциях есть важный момент того, что типы a и b должны быть одинаковыми по отношению друг к другу. Если a и b не являются значениями одного типа, то будет невозможно поменять их значения местами. Swift является типо-безопасным языком и не позволяет (например) переменным с типом String меняться значениями с типом Double. Попытка сделать это приведет к ошибке компиляции.

Универсальные функции

Универсальные функции могут работать с любыми типами. Ниже приведена универсальная версия функции swapTwoInts(_:_:), которая теперь называется swapTwoValues(_:_:):

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {

let temporaryA = a

a = b

b = temporaryA

}

Тело функции swapTwoValues(_:_:) идентично телу функции swapTwoInts(_:_:). Однако первая строка функции swapTwoValues(_:_:) немного отличается от аналогичной строки функции swapTwoInts(_:_:). Вот как можно сравнить первые строки этих функций:

func swapTwoInts(_ a: inout Int, _ b: inout Int)

func swapTwoValues<T>(_ a: inout T, _ b: inout T)

Универсальная версия использует заполнитель имени типа (называется T в нашем случае) вместо текущего имени типа (Int, String, Double…). Заполнитель имени типа ничего не говорит о том, чем должно являться T, но он говорит о том, что и a и b должны быть одного типа T, независимо от того, что такое T. Текущий тип T будет определяться каждый раз, как вызывается функция swapTwoValues(_:_:).

Другое отличие в том, что за именем универсальной функции (swapTwoValues(_:_:)) идет заполнитель имени типа (Т) в угловых скобках (<T>). Угловые скобки говорят Swift, что T является заполнителем имени типа внутри определения функции swapTwoValues(_:_:). Так как T является заполнителем, то Swift не смотрит на текущее значение T.

Функция swapTwoValues(_:_:) теперь может быть вызвана точно так же как и функция swapTwoInts, за исключением того, что в нее можно передавать значения любого типа, до тех пор пока они одного типа. Каждый раз при вызове функции swapTwoValues(_:_:), тип Т выводится из типов, которые передаются в эту функцию.

В двух примерах ниже T имеет значение типа Int и String соответственно:

var someInt = 3

var anotherInt = 107

swapTwoValues(&someInt, &anotherInt)

// someInt равен 107, а anotherInt равен 3

var someString = "hello"

var anotherString = "world"

swapTwoValues(&someString, &anotherString)

// someString равна "world", а anotherString равна "hello"

Заметка

Указанная выше функция swapTwoValues(_:_:) появилась под влиянием функции, называемой swap, которая является частью стандартной бибилиотеки Swift и предоставляется для использования в ваших приложениях. Если вам требуется поведение функции swapTwoValues(_:_:) в вашем коде, вы можете использовать существующую в Swift функцию swap(_:_:), а не писать свою собственную реализацию.

Параметры типа

В примере выше в функции swapTwoValues(_:_:) заполнитель имени типа T пример параметра типа. Параметры типа определяют и называют тип наполнителя, и пишутся сразу после имени функции, между угловыми скобками (например, <T>).

Как только вы определили параметр типа, то вы можете использовать его в качестве типа параметра функции (как например, параметры a и b в функции swapTwoValues(_:_:)) или как возвращаемый функциональный тип, или как аннотация типа внутри тела функции. В каждом случае заполнитель типа отображается параметром типа, который заменяется на актуальное значение типа при вызове функции. (В swapTwoValues(_:_:) в нашем примере выше произошло замещение Т на Int в момент, когда функция была вызвана в первый раз, и на String при повторном вызове.)

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

Именование параметров типа

В большинстве случаев параметры типа имеют описательные имена, такие как Key и Value в Dictionary<Key, Value> и Element в Array<Element>, которые помогут читающему код определить взаимосвязь между параметром типа и универсальным типом или функцией, в которых он используется. Тем не менее, когда между ними нет значимых отношений, то по традиции именами становятся отдельные буквы, такие как T, U, V, как например T в функции swapTwoValues(_:_:).

Заметка

Всегда давайте параметрам типа имена "горбатого" верхнего регистра (например, T и MyTypeParameter), чтобы указать, что они являются заполнителем для типа, а не значением.

Универсальные типы

В дополнение к универсальным функциям, Swift позволяет вам определять ваши универсальные типы. Это к примеру универсальные классы, структуры и перечисления, которые могут работать с любыми типами, наподобие тому, как работают Array или Dictionary.

Эта секция покажет вам как создать вашу универсальную коллекцию типа Stack. Stack - упорядоченная коллекция значений, аналогичная массиву, но с более строгим набором операций, чем имеет тип Array языка Swift. Массив позволяет вам вставлять и удалять элементы с любой позиции массива. Однако, Stack позволяет добавлять новые элементы только в конец коллекции (известно как заталкивание (или pushing на англ) нового значения в стек). Аналогично стек позволяет удалять элементы только с конца коллекции (известно как выстреливать значение из стека (или popping по англ).

Заметка

Концепция стека используется классом UINavigationController для моделирования контроллеров видов в его иерархии навигации. Вы вызываете метод pushViewController(_:animated:) класса UINavigationController для добавления контроллера вида на стек навигации, а метод popViewControllerAnimated(_:) для удаления контроллера вида из стека навигации. Стек - полезная модель коллекции, когда вам нужен строгий принцип “последний на вход - первый на выход”.

Ниже приведена иллюстрация поведения добавления и удаления элемента из стека:

На данный момент у нас три значения в стеке.

Четвертое значение “затолкнули” на самый верх стека.

На этот момент в стеке находится четыре значения, самое свежее значение находится наверху.

Последнее значение удалено или “выстреляно” из стека.

После удаления значения, стек снова имеет три значения.

Вот как написать неуниверсальную версию стека, в этом случае мы используем стек для хранения Int значений:

struct IntStack {

var items = [Int]()

mutating func push(_ item: Int) {

items.append(item)

}

mutating func pop() -> Int {

return items.removeLast()

}

}

Эта структура использует свойство items типа Array для хранения значений в стеке. Stack предоставляет нам два метода, push и pop для добавления последнего элемента в стек и для удаления последнего элемента из стека. Эти методы отмечены как mutating, потому как они вынуждены менять массив items.

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

Вот универсальная версия структуры:

struct Stack<Element> {

var items = [Element]()

mutating func push(_ item: Element) {

items.append(item)

}

mutating func pop() -> Element {

return items.removeLast()

}

}

Обратите внимание как универсальная версия Stack похожа на не универсальную, вообще отличаясь только тем, что мы используем заполнитель типа, вместо указания конкретного типа Int. Этот параметр типа написан внутри угловых скобок ( <Element>), сразу после имени структуры.

Element определяет заполнитель имени типа для “какого-то типа Element”, который будет предоставлен позже. Этот будущий тип может ссылаться на Element в любом месте определения структуры. В этом случае наш некоторый тип Element используется в трех местах:

Для создания свойства items, которое инициализируется пустым массивом типа Element.

Для указания того, что метод push(_:) имеет единственный параметр item, который должен быть типа Element.

Для указания типа возвращаемого значения методом pop(), которое должно быть типом Element.

Из-за того, что это является универсальным типом, то Stack может быть использован для создания стека любых корректных типов в Swift, аналогичным образом как это осуществляют типы Array или Dictionary.

Вы создаете новый экземпляр Stack, вписав тип хранимых значений стека в угловые скобки. Например, создадим новый стек строк, вы напишите Stack<String>():

var stackOfStrings = Stack<String>()

stackOfStrings.push("uno")

stackOfStrings.push("dos")

stackOfStrings.push("tres")

stackOfStrings.push("cuatro")

// stack содержит 4 строки

Теперь stackOfStrings выглядит вот так после добавления последних четырех значений:

Удаляя последнее значение, он возвращает его и удаляет его из стека “cuatro”:

let fromTheTop = stackOfStrings.pop()

// fromTheTop равен "cuatro", а stack содержит 3 строки

После удаления верхней величины, стек выглядит так:

Расширяем универсальный тип

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

Следующий пример расширяет универсальный тип Stack, для добавления вычисляемого свойства только для чтения topItem, которое возвращает верхний элемент стека, без “выстреливания” его из этого стека:

extension Stack {

var topItem: Element? {

return items.isEmpty ? nil : items[items.count - 1]

}

}

Свойство topItem возвращает опциональное значение типа Element. Если стек пустой, то topItem возвращает nil. Если стек не пустой, то topItem возвращает последний элемент массива items.

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

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

if let topItem = stackOfStrings.topItem {

print("The top item on the stack is \(topItem).")

}

// Выведет "The top item on the stack is tres."

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

Ограничения типа

Функция swapTwoValues(_:_:) и тип Stack могут работать с любыми типами. Однако иногда бывает нужно внедрить определенные ограничения типа на типы, которые могут быть использованы вместе универсальными функциями или универсальными типами. Ограничения типа указывают на то, что параметры типа должны наследовать от определенного класса или соответствовать определенному протоколу или композиции протоколов.

Для примера возьмем тип Dictionary, который имеет некоторые ограничения типов, которые могут быть использованы в качестве ключей. Как было описано в главе Словари, тип ключа словаря должен быть хешируемым. Таким образом он должен предоставить способ представить себя уникальным. Dictionary нужно, чтобы его ключи были хешируемыми, таким образом он может проверить, содержит ли конкретный ключ какое-либо значение. Без этого требования, Dictionary не в состоянии понять, должен ли он заменить или вставить значение для конкретного ключа, и не в состоянии найти значение для конкретного ключа, которое уже есть в словаре.

Такое требование внедряется ограничениями типа для типа ключа словаря, которое определяет, что каждый ключ должен соответствовать протоколу Hashable, специальному протоколу, который определен в стандартной библиотеке Swift. Все базовые типа Swift (String, Int, Double, Bool) по умолчанию являются хешируемыми типами.

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

Синтаксис ограничения типа

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

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {

// тело функции…

}

Выше описанная гипотетическая функция имеет два параметра типа. Первый параметр типа - T, имеет ограничение типа, которое требует чтобы T, было подклассом класса SomeClass. Второй параметр типа - U, имеет ограничение типа, которое требует чтобы U соответствовал протоколу SomeProtocol.

Ограничение типа в действии

Ниже приведена неуниверсальная функция findIndex(ofString:in:), которая получает значение типа String для того, чтобы его найти, и массив значений типа String, внутри которого и будет происходить поиск. Функция findIndex(ofString:in:) возвращает опциональное значение Int, которое является индексом первого совпадения строки с элементом внутри массива или nil, которое означает отсутствие совпадения строки с каким-либо элементом массива:

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {

for (index, value) in array.enumerated() {

if value == valueToFind {

return index

}

}

return nil

}

Функция findIndex(ofString:in:) может быть использована для поиска строкового значения в массиве строк:

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]

if let foundIndex = findIndex(ofString: "llama", in: strings) {

print("The index of llama is \(foundIndex)")

}

// Выведет "The index of llama is 2"

Однако нахождение индекса совпадения значения в массиве бывает полезным не только для строк. Вы можете написать ту же функцию, но только в универсальной форме. Давайте напишем такую функцию и назовем ее findIndex, а все упоминания типа String заменим на тип T.

Вот как будет выглядит версия функции findIndex(ofString:in:) в универсальной форме findIndex(of:in:). Обратите внимание, что возвращаемый функцией тип все еще равен Int?, потому что функция возвращает опциональное значение индекса, а не опциональное значение элемента массива. Но будьте осторожны, так как эта функция не компилируется, по причинам, указанным после примера:

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {

for (index, value) in array.enumerated() {

if value == valueToFind {

return index

}

}

return nil

}

Как мы и сказали, эта функция не компилируется. Проблема находится в строке “if value == valueToFind”. Не каждый тип в Swift может быть сравнен оператором равенства (==). Если вы создаете свой класс или структуру для отображения сложной модели данных, например, то смысл выражения “равен чему-то” для этого класса или структуры Swift не может додумать за вас. Из-за этого нет никакой гарантии того, что этот код будет работать для любого возможного класса T, и соответствующая ошибка компиляции выскакивает, когда вы пытаетесь скомпилировать код.

Но не все еще потеряно. Стандартная библиотека Swift определяет протокол Equatable, который требует любой соответствующий ей тип реализовывать равенство оператору равенства (==) и реализовывать неравенство оператору неравенства (!=), для того, чтобы значения этих типов можно было сравнивать между собой. Все стандартные типы Swift автоматически поддерживают протокол Equatable.

Любой тип, который удовлетворяет протоколу Equatable, может быть безопасно использован в функции findIndex(of:in:), потому что гарантирована поддержка оператора равенства и неравенства. Для отображения этого факта, вы пишите ограничение типа Equitable, как часть определения параметра типа, когда вы определяете функцию:

func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {

for (index, value) in array.enumerated() {

if value == valueToFind {

return index

}

}

return nil

}

Единственный параметр типа для функции findIndex(of:in:) записывается как T: Equatable, что означает “любой тип T, который соответствует протоколу Equatable”.

Теперь функция findIndex(of:in:) благополучно компилируется и может быть использована с любыми типами Equatable, например, String, Double:

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])

// doubleIndex опциональный Int не имеющий значения, потому что значения 9.3 нет в массиве

let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])

// stringIndex опциональный Int равный 2

Связанные типы

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

Связанные типы в действии

Ниже приведен пример протокола Container, который объявляет связанный тип Item:

protocol Container {

associatedtype Item

mutating func append(_ item: Item)

var count: Int { get }

subscript(i: Int) -> Item { get }

}

Протокол Container определяет три требуемых возможности, которые должен иметь любой контейнер:

Должна быть возможность добавлять новый элемент в контейнер при помощи метода append(_:).

Должна быть возможность получить доступ к количеству элементов в контейнере через свойство count, которое возвращает значение типа Int.

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

Этот протокол не указывает количество и способ хранения элементов в контейнере или какого типа они должны быть. Протокол только лишь указывает три “кусочка” функциональности, которые должны быть предоставлены контейнером, чтобы он считался Container. Соответствующий тип может предоставлять дополнительную функциональность, пока он удовлетворяет этим трем требованиям.

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

Для определения этих требований, протокол Container должен иметь способ ссылаться на тип элементов, которые он будет хранить, без указания типа элементов, которые может хранить конкретный контейнер. Протокол Container должен указать, что любое значение переданное в метод append(_:) должно иметь тот же тип, что и тип элементов контейнера, и что значение, возвращаемое сабскриптом контейнера, должно быть того же типа, что и элементы контейнера.

Чтобы добиться этого, протокол Container объявляет связанный тип ItemType, который записывается как associatedtype Item. Протокол не определяет для чего конкретно нужен алиас ItemType, потому что эта информация остается для любого соответствующего класса протоколу. Тем не менее, алиас ItemType предоставляет способ сослаться на тип элементов в Container и определить тип для использования метода append(_:) и сабскрипта, для того, чтобы гарантировать, что желаемое поведение любого Container имеет силу.

Ниже приведена версия неуниверсального типа IntStack, который адаптирован под протокол Container:

struct IntStack: Container {

// исходная реализация IntStack

var items = [Int]()

mutating func push(_ item: Int) {

items.append(item)

}

mutating func pop() -> Int {

return items.removeLast()

}

// удовлетворение требований протокола Container

typealias Item = Int

mutating func append(_ item: Int) {

self.push(item)

}

var count: Int {

return items.count

}

subscript(i: Int) -> Int {

return items[i]

}

}

Тип IntStack реализует все три требования протокола Container, и в каждом случае оборачивает часть существующей функциональности типа IntStack для удовлетворения этих требований.

Более того, IntStack указывает, что для этой реализации контейнера, подходящий тип Item будет Int. Определение typealias Item = Int преобразует абстрактный тип ItemType в конкретный тип Int для этой реализации протокола Container.

Благодаря выводу типов Swift, вам фактически не нужно указывать конкретный тип Int для Item как часть определения IntStack. Так как IntStack соответствует протоколу Container, Swift может вывести соответствующий тип для Item, просто посмотрев на тип параметра item метода append(_:) и на тип возвращаемого значения сабскрипта. И на самом деле, если удалить строку кода typealias Item = Int, все будет продолжать работать, потому что все еще ясно какой тип должен быть использован для Item.

Вы так же можете создать универсальный тип Stack, который соответствует протоколу Container:

struct Stack<Element>: Container {

// исходная реализация Stack<Element>

var items = [Element]()

mutating func push(_ item: Element) {

items.append(item)

}

mutating func pop() -> Element {

return items.removeLast()

}

// удовлетворение требований протокола Container

mutating func append(_ item: Element) {

self.push(item)

}

var count: Int {

return items.count

}

subscript(i: Int) -> Element {

return items[i]

}

}

В этот раз тип параметра Element использован в качестве параметра item метода append(_:) и в качестве возвращаемого типа сабскрипта. Таким образом Swift может вывести, что Element подходящий тип для использования его в качестве ItemType для этого конкретного контейнера.

Расширение существующего типа для указания связанного типа

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

Тип Array уже предоставляет нам метод append(_:), свойство count и сабскрипт со значением индекса типа Int для получения его элементов. Эти три возможности удовлетворяют совпадают с требованиями протокола Container. Это означает, что вы можете расширить тип Array, чтобы он соответствовал протоколу Container, просто указав, что Array принимает протокол Container. Вы можете сделать это при помощи пустого расширения, которое подробнее описано в подразделе главы Принятие протокола через расширение:

extension Array: Container {}

Существующий метод append(_:) типа Array и сабскрипт позволяют Swift выводить соответствующий тип для ItemType, точно так же как и для универсального типа Stack, который был приведен ранее. После определения расширения вы можете использовать Array как Container.

Использование аннотаций типа для ограничения связанного типа

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

protocol Container {

associatedtype Item: Equatable

mutating func append(_ item: Item)

var count: Int { get }

subscript(i: Int) -> Item { get }

}

Для соответствия данной версии Container, каждому элементу Container нужно соответствовать/реализовывать протокол Equatable.

Использование протокола в ограничениях связанного типа

Протокол может выступать как часть собственных требований. Например, ниже в примере, есть протокол, который уточняет протокол Container, добавляя метод suffix(_:). Метод suffix(_:) возвращает заданное количество элементов из конца контейнера, сохраняя их в экземпляре типа Suffix.

protocol SuffixableContainer: Container {

associatedtype Suffix: SuffixableContainer where Suffix.Item == Item

func suffix(_ size: Int) -> Suffix

}

В этом протоколе Suffix является ассоциированным типом, подобным типу Item в приведенном выше примере. Suffix имеет два ограничения: он должен соответствовать протоколу SuffixableContainer (протокол, который в настоящее время определяется), а его тип Item должен быть таким же, как тип Item контейнера. Ограничение на Item является общей оговоркой where, которое обсуждается ниже.

Ниже расширение типа Stack, которое добавляет соответствие протоколу SuffixableContainer:

extension Stack: SuffixableContainer {

func suffix(_ size: Int) -> Stack {

var result = Stack()

for index in (count-size)..<count {

result.append(self[index])

}

return result

}

// Inferred that Suffix is Stack.

}

var stackOfInts = Stack()

stackOfInts.append(10)

stackOfInts.append(20)

stackOfInts.append(30)

let suffix = stackOfInts.suffix(2)

// suffix contains 20 and 30

В примере выше, ассоциативный тип Suffix для Stack является тем же Stack, так что операция suffix в Stack вернет другой Stack. Альтернативно, тип который соответствует требованиям протокола SuffixableContainer может иметь тип Suffix, который отличается от самого себя, что означает, что операция suffix может вернуть другой тип. Например, ниже расширение для IntStack, который добавляет соответствие протоколу SuffixableContainer, используя Stack в качестве суффиксного типа, вместо IntStack.

extension IntStack: SuffixableContainer {

func suffix(_ size: Int) -> Stack<Int> {

var result = Stack<Int>()

for index in (count-size)..<count {

result.append(self[index])

}

return result

}

// Inferred that Suffix is Stack<Int>.

}

Оговорка where

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

Так же бывает полезно определять требования для связанных типов. Чтобы сделать это, вы определяете оговорку where, как часть списка параметров типа. Оговорка where позволяет вам требовать, чтобы связанный тип, соответствовал определенному протоколу, и/или чтобы конкретные параметры типа и связанные типы были одними и теми же. Вы пишите оговорку where, поместив ключевое слово where сразу после списка параметров типа, за которым следует одно или более ограничений для связанных типов, и/или один или более отношений равенства между типами и связанными типами.

В примере ниже определяем универсальную функцию allItemsMatch, которая проверяет, чтобы увидеть содержат ли два экземпляра Container одни и те же элементы в одной и той же последовательности. Функция возвращает значение типа Bool, то есть, если у нас все элементы и их последовательность совпадает, то функция возвращает true, если нет - false.

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

func allItemsMatch<C1: Container, C2: Container>

(_ someContainer: C1, _ anotherContainer: C2) -> Bool

where C1.Item == C2.Item, C1.Item: Equatable {

// Проверяем одинаковое ли количество элементов находится в контейнерах.

if someContainer.count != anotherContainer.count {

return false

}

// Проверяем все ли значения попарно равны.

for i in 0..<someContainer.count {

if someContainer[i] != anotherContainer[i] {

return false

}

}

// Все элементы совпадают, так что возвращаем true.

return true

}

Эта функция принимает два аргумента someContainer и anotherContainer. Аргумент someContainer имеет тип C1, аргумент anotherContainer имеет тип C2. И C1 и C2 являются заполнителями имен типов для двух контейнеров, которые будут определены, когда будет вызвана функция.

Список типов параметров функции размещает следующие требования на два параметров типа:

C1 должен соответствовать протоколу Container (C1: Container)

C2 должен соответствовать протоколу Container (C2: Container)

Item для C1 должен быть тем же, что и Item для C2 (C1.Item == C2.Item)

Item для C1 должен соответствовать протоколу Equatable (C1.Item: Equatable)

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

Эти требования означают:

someContainer является контейнером типа C1.

anotherContainer является контейнером типа C2.

someContainer и anotherContainer содержат значения одного типа

Элементы в someContainer могут быть проверены при помощи оператора неравенства (!=), чтобы увидеть, что они отличаются друг от друга.

Третье и четвертое требование комбинируются так, чтобы элементы в anotherContainer так же могли бы быть проверены оператором !=, потому что они в точности одного и того же типа, что и в someContainer.

Эти требования позволяют функции allItemsMatch(_:_:) сравнивать два контейнера, даже если они являются контейнерами разного типа.

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

После проведения этой проверки, функция перебирает все элементы в someContainer при помощи for-in цикла и полуоткрытого оператора диапазона (..<). Для каждого элемента someContainer функция проверяет равенство элемента соответствующему элементу в контейнере anotherContainer. Если два элемента не равны друг другу, то эти два контейнера не считаются одинаковыми, функция возвращает false.

Если цикл закончился без каких-каких либо несоответствий элементов, то два контейнера считаются одинаковыми, и функция возвращает true.

Вот как выглядит функция allItemsMatch(_:_:) в действии:

var stackOfStrings = Stack<String>()

stackOfStrings.push("uno")

stackOfStrings.push("dos")

stackOfStrings.push("tres")

var arrayOfStrings = ["uno", "dos", "tres"]

if allItemsMatch(stackOfStrings, arrayOfStrings) {

print("All items match.")

} else {

print("Not all items match.")

}

// Выведет "All items match."

Пример выше создает экземпляр Stack для хранения значений типа String и добавляет три значения в стек. Так пример создает экземпляр Array, который инициализируется литералом массива, содержащего три одинаковые строки в стеке. Даже тогда стек и массив имеют разные типы, но оба они соответствуют протоколу Container, и оба они содержат одинаковый тип значений. Тем не менее вы можете вызвать функцию allItemsMatch(_:_:) с этими двумя контейнерами в качестве своих аргументов. В примере выше функция allItemsMatch(_:_:) корректно извещает нас, что все элементы этих двух контейнеров одинаковые.

Расширения с оговоркой where

Вы так же можете использовать оговорку  where в расширениях. В примере ниже у нас есть раширение для структуры Stack из прошлого примера, где мы добавляем метод  isTop(_:).

extension Stack where Element: Equatable {

func isTop(_ item: Element) -> Bool {

guard let topItem = items.last else {

return false

}

return topItem == item

}

}

Новый метод isTop(_:) сначала проверяет, что наш стек не пустой, а затем сравнивает верхний элемент стека с данным нам item. Если вы попытаетесь сделать то же самое без универсальной where, то у вас будут проблемы. Реализация  isTop(_:) использует оператор ==, но определение Stack не требует того, чтобы элементы могли сравниваться, так что написание оператора  == вызовет ошибку компиляции. Использование универсальной  where позволяет вам добавить новое требование к расширению, так что расширение добавляет метод isTop(_:) только в том случае, если элементы в Stack реализуют Equatable.

Вот как будет выглядеть наш метод isTop(_:) в действии:

if stackOfStrings.isTop("tres") {

print("Top element is tres.")

} else {

print("Top element is something else.")

}

// Выведет "Top element is tres."

Если вы попытаетесь вызвать метод isTop(_:) в стеке, то те, элементы, которые не реализуют протокол Equatable вызовут ошибку компиляции:

struct NotEquatable { }

var notEquatableStack = Stack<NotEquatable>()

let notEquatableValue = NotEquatable()

notEquatableStack.push(notEquatableValue)

notEquatableStack.isTop(notEquatableValue) // Error

Вы можете использовать универсальную where с расширениями протоколов. Пример ниже расширяет протокол Container из прошлого примера, добавляя ему новый метод startsWith(_:).

extension Container where Item: Equatable {

func startsWith(_ item: Item) -> Bool {

return count >= 1 && self[0] == item

}

}

Метод startsWith(_:) сначала проверяет на наличие элементов в контейнере, а затем проверяет не равен ли данный элемент первому элементу контейнера. Новый метод startsWith(_:) может быть использован с любым типом, который реализует протокол Container, включая стеки, массивы, использованные ранее, элементы контейнера соответствуют протоколу Equatable.

if [9, 9, 9].startsWith(42) {

print("Starts with 42.")

} else {

print("Starts with something else.")

}

// Выведет "Starts with something else."

Универсальная оговорка where в примере выше требует, чтобы Item соответствовал протоколу Container, но вы так же можете использовать where для указания конкретного типа для Item. Например:

extension Container where Item == Double {

func average() -> Double {

var sum = 0.0

for index in 0..<count {

sum += self[index]

}

return sum / Double(count)

}

}

print([1260.0, 1200.0, 98.6, 37.0].average())

// Выведет "648.9"

Этот пример добавляет контейнерам метод average(), у которых Item имеет тип Double. Он итерирует по элементам контейнера, суммируя их, и делит сумму на их общее количество. Он явно преобразует count из Int в Double что позволяет нам проводить деление чисел с плавающей точкой.

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

Связанные типы с универсальной оговоркой where

Вы можете включать универсальную оговорку where в связанный тип. Например, предположим, что вы хотите создать версию своего Container, которая включает в себя итератор, точно так же как и протокол Sequence использует стандартную библиотеку. Вот как вы можете это записать:

protocol Container {

associatedtype Item

mutating func append(_ item: Item)

var count: Int { get }

subscript(i: Int) -> Item { get }

associatedtype Iterator: IteratorProtocol where Iterator.Element == Item

func makeIterator() -> Iterator

}

Универсальная оговорка where в Iterator требует, чтобы итератор должен поддерживать тот же самый тип элементов, что и тип элементов контейнера, не смотря на тип самого итератора. Метод makeIterator() предоставляет доступ к итератору контейнера.

Для протокола, который наследуется от другого протокола, вы можете добавить ограничение в унаследованный связанный тип, путем включения универсальной оговорки where в объявление протокола. Например следующий код объявляет протокол ComparableContainer, который требует, чтобы Item соответствовал протоколу Comparable:

protocol ComparableContainer: Container where Item: Comparable { }

Универсальные сабскрипты

Сабскрипты могут быть универсальными, и они могут включать в себя универсальную оговорку where. Вы можете написать имя-плейсхолдер внутри угловых скобок после ключевого слова subscript, и вы пишите универсальную оговорку where прямо до открывающей фигурной скобки тела сабскрипта. Например:

extension Container {

subscript<Indices: Sequence>(indices: Indices) -> [Item]

where Indices.Iterator.Element == Int {

var result = [Item]()

for index in indices {

result.append(self[index])

}

return result

}

}

Расширения протоколу Container добавляет сабскрипт, который принимает последовательность индексов и возвращает массив, содержащий элементы указанных индексов. Универсальный сабскрипт имеет следующие ограничения:

Универсальный параметр indices в угловых скобках, должен быть типа, который реализует протокол Sequence из стандартной библиотеки.

Сабскрипт принимает один параметр, indices, который является экземпляром типа indices.

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

Беря все это во внимание, ограничения, которые описаны выше означают, что значения, передаваемые для параметра indices, должны быть последовательностью элементов типа Int.

Автоматический подсчет ссылок (ARC)

Swift использует automatic reference counting (автоматический подсчет ссылок) для отслеживания и управления памятью вашего приложения. В большинстве случаев это означает, что управление памятью "просто работает" в Swift и вам не нужно думать о самостоятельном управлении памятью. ARC автоматически освобождает память, которая использовалась экземплярами класса, когда эти экземпляры больше нам не нужны.

Однако, в некоторых случаях для управления памятью ARC нужно больше информации об отношениях между некоторыми частями вашего кода. Эта глава опишет эти случаи и покажет как включить ARC, чтобы эта система взяла на себя весь контроль памятью вашего приложения. Использование ARC в Swift очень схоже с использованием ARC в Objective-C, описание которого можно найти в Transitioning to ARC Release Notes.

ARC применима только для экземпляров класса. Структуры и перечисления являются типами значений, а не ссылочными типами, и они не хранятся и не передают свои значения по ссылке.

Работа ARC

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

Дополнительно, когда экземпляр больше не нужен, ARC освобождает память, использованную под этот экземпляр, и направляет эту память туда, где она нужна. Это своего рода гарантия того, что ненужные экземпляры не будут занимать память.

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

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

Для того чтобы это было возможно, каждый раз как вы присваиваете экземпляр свойству, константе или переменной создается strong reference (сильная ссылка) с этим экземпляром. Такая связь называется “сильной”, так как она крепко держится за этот экземпляр и не позволяет ему освободится до тех пор, пока остаются сильные связи.

ARC в действии

Приведем пример того, как работает ARC. Наш пример начнем с класса Person, который определяет константное свойство name:

class Person {

let name: String

init(name: String) {

self.name = name

print("\(name) инициализируется")

}

deinit {

print("\(name) деинициализируется")

}

}

Класс Person имеет инициализатор, который устанавливает name свойство экземпляра и выводит сообщение для отображения того, что идет инициализация. Так же класс Person имеет деинициализатор, который выводит сообщение, когда экземпляр класса освобождается.

Следующий фрагмент кода определяет три переменные класса Person?, который используется для установки нескольких ссылок к новому экземпляру Person в следующих кусках кода. Так как эти переменные опционального типа (Person?, а не Person), они автоматически инициализируются со значением nil, и не имеют никаких ссылок на экземпляр Person.

var reference1: Person?

var reference2: Person?

var reference3: Person?

Теперь вы можете создать экземпляр класса Person и присвоить его одной из этих трех переменных:

reference1 = Person(name: "John Appleseed")

// Выведет "John Appleseed инициализируется"

Обратите внимание, что сообщение "John Appleseed инициализируется" выводится во время того, как вы вызываете инициализатор класса Person. Это подтверждает тот факт, что происходила инициализация.

Так как новый экземпляр класса Person был присвоен переменной reference1, значит теперь существует сильная ссылка между reference1 и новым экземпляром класса Person. Теперь у этого экземпляра есть как минимум одна сильная ссылка, значит ARC держит под Person память и не освобождает ее.

Если вы присвоите другим переменным тот же экземпляр Person, то добавится две сильные ссылки к этому экземпляру:

reference2 = reference1

reference3 = reference1

Теперь экземпляр класса Person имеет три сильные ссылки.

Если вы сломаете две из этих трех ссылок (включая и первоначальную ссылку), присвоим nil двум переменным, то останется одна сильная ссылка, и экземпляр Person не будет освобожден:

reference1 = nil

reference2 = nil

ARC не освободит экземпляр класса Person до тех пор, пока остается последняя сильная ссылка, уничтожив которую мы укажем на то, что наш экземпляр больше не используется:

reference3 = nil

// Выведет "John Appleseed деинициализируется"

Циклы сильных ссылок между экземплярами классов

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

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

Вы сами решаете, когда сделать вместо сильной (strong) ссылки слабую (weak) или бесхозную (unowned). Подробнее об этом можно прочитать в разделе Замена циклов сильных ссылок между экземплярами классов. Однако перед тем как узнать, в каких случаях разрешить сильный ссылочный цикл, давайте узнаем что вызывает его.

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

class Person {

let name: String

init(name: String) { self.name = name }

var apartment: Apartment?

deinit { print("\(name) освобождается") }

}

class Apartment {

let unit: String

init(unit: String) { self.unit = unit }

var tenant: Person?

deinit { print("Апартаменты \(unit) освобождаются") }

}

Каждый экземпляр Person имеет свойство name типа String и опциональное свойство apartment, которое изначально nil. Свойство apartment опционально, так как наша персона не обязательно всегда должна иметь апартаменты.

Аналогично, что каждый экземпляр Apartment имеет свойство unit типа String и опциональное свойство tenant, которое изначально nil. Свойство tenant опциональное, потому как не всегда в апартаментах кто-то живет.

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

Следующий фрагмент кода определяет две опциональные переменные с именами john и unit4A, которые будут назначены определенным экземплярам классов Apartment и Person. Оба значения переменных равны nil, в силу того, что они опциональны:

var john: Person?

var unit4A: Apartment?

Теперь вы можете создать свои экземпляры Person и Apartment и присвоить их этим переменным john, unit4A:

john = Person(name: "John Appleseed")

unit4A = Apartment(unit: "4A")

Вот как выглядят сильные связи после того создания и присваивания этих двух экземпляров. Переменная john имеет сильную связь с экземпляром класса Person, переменная unit4A имеет сильную связь с экземпляром Apartment:

Теперь вы можете соединить эти два экземпляра вместе, так что житель будет иметь апартаменты, а апартаменты будут иметь своих жителей. Обратите внимание, что восклицательный знак (!) используется для развертывания и допуска к экземплярам, хранимым в опциональных переменных john, unit4A, так что установить значения свойством можно в такой форме:

john!.apartment = unit4A

unit4A!.tenant = john

Вот как выглядят сильные связи после того, как мы соединили экземпляры:

К сожалению, соединяя таким образом, образуется цикл сильных ссылок между экземплярами. Экземпляр Person имеет сильную ссылку на экземпляр Apartment, экземпляр Apartment имеет сильную ссылку на экземпляр Person. Таким образом, когда вы разрушаете сильные ссылки, принадлежащие переменным john и unit4A, их количество все равно не падает до нуля, и экземпляры не освобождаются:

john = nil

unit4A = nil

Обратите внимание, что ни один деинициализатор не был вызван, когда вы присваивали nil. Цикл сильных ссылок предотвратил экземпляры Person и Apartment от освобождения, что вызывает утечку памяти в вашем приложении.

Вот как выглядят сильные ссылки после того, как вы присвоили nil переменным, john, unit4A:

Сильные взаимные ссылки остались между экземплярами Person и Apartment и не могут быть разрушены.

Замена циклов сильных ссылок между экземплярами классов

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

Слабые и безхозные ссылки позволяют одному экземпляру в цикле ссылок ссылаться на другой экземпляр без сильного прикрепления. Экземпляры могут ссылаться друг на друга без создания цикла сильных связей.

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

Слабые (weak) ссылки

Слабые ссылки не удерживаются за экземпляр, на который они указывают, так что ARC не берет их во внимание, когда считает ссылки экземпляра. Такой подход позволяет избежать ситуации, когда ссылка становится частью цикла сильных ссылок. Вы указываете слабую ссылку ключевым словом weak перед именем объявляемого свойства или переменной.

Так как слабая ссылка не сильно держится за экземпляр, на который указывает, то можно освободить экземпляр, на который указывает такая ссылка. Таким образом ARC автоматически присваивает слабой ссылке nil, когда экземпляр, на который она указывает, освобождается. И поскольку слабые ссылки должны позволять изменять их значение до нуля во время выполнения, они всегда объявляются как переменные, а не как константы опционального типа.

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

Заметка

Когда ARC устанавливает слабую ссылку на nil, наблюдатели свойств не вызываются.

Пример ниже идентичен тому, что мы разбирали с вами с классами Person, Apartment, но только теперь в нем есть одно существенное отличие. В этот раз свойство tenant экземпляра класса Apartment объявлено как слабая ссылка:

class Person {

let name: String

init(name: String) { self.name = name }

var apartment: Apartment?

deinit { print("\(name) деинициализируется") }

}

class Apartment {

let unit: String

init(unit: String) { self.unit = unit }

weak var tenant: Person?

deinit { print("Apartment \(unit) деинициализируется") }

}

Создадим как и в предыдущем примере сильные ссылки от двух переменных (john, unit4A) и связи между двумя экземплярами:

var john: Person?

var unit4A: Apartment?

john = Person(name: "John Appleseed")

unit4A = Apartment(unit: "4A")

john!.apartment = unit4A

unit4A!.tenant = john

Вот как теперь выглядит соединение двух экземпляров между собой:

Экземпляр Person все еще имеет сильную ссылку на экземпляр Apartment, но Apartment имеет слабую (weak) ссылку на экземпляр Person. Это означает, что когда вы разрушаете сильную ссылку, которая содержится в переменной john, то больше сильных ссылок, указывающих на экземпляр Person, не остается:

john = nil

// Выведет "John Appleseed деинициализируется"

А так как больше сильных ссылок на экземпляр Person нет, то свойство tenant становится равным nil:

Остается только одна сильная ссылка на экземпляр Apartment из переменной unit4A. Если вы разрушите эту сильную ссылку, то их общее количество станет равным нулю:

unit4A = nil

// выводит "Апартаменты 4A деинициализируется"

А так как больше сильных ссылок нет, то и экземпляр Apartment тоже освобождается:

Заметка

Там, где используются сборщики "мусора", слабые указатели иногда используются для реализации простого механизма кеширования, потому что объекты без сильных связей сразу отпускаются, как только у памяти появляется необходимость избавится от "мусора". Однако со включенной ARC значения удаляются только тогда, когда уходит последняя сильная связь на них, делая слабые связи не подходящими для текущей задачи.

Бесхозные ссылки

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

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

Заметка

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

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

Следующий пример определяет два класса Customer и CreditCard, которые обыгрывают ситуацию клиента банка и кредитной карточки для этого клиента. Эти оба класса содержат экземпляры друг друга в качестве свойства. Такое взаимоотношение классов является потенциальной возможностью образования зацикливания сильных ссылок.

Взаимоотношения между Customer и CreditCard немного отличаются от предыдущего примера с Apartment и Person. В этом случае клиент может иметь или не иметь кредитной карты, но кредитная карта всегда имеет владельца. Чтобы это отобразить, класс Customer имеет опциональное свойство card, а CreditCard имеет неопциональное свойство customer.

Более того, новый экземпляр CreditCard может быть только создан путем передачи значения number и экземпляра customer в инициализатор класса CreditCard. Это гарантирует, что экземпляр CreditCard всегда будет иметь экземпляр customer, который будет связан с ним, когда экземпляр CreditCard будет создан.

Так как кредитная карта всегда будет иметь своего хозяина, вы определяете свойство customer как бесхозное, для избежания цикла сильных ссылок:

class Customer {

let name: String

var card: CreditCard?

init(name: String) {

self.name = name

}

deinit { print("\(name) деинициализируется") }

}

class CreditCard {

let number: UInt64

unowned let customer: Customer

init(number: UInt64, customer: Customer) {

self.number = number

self.customer = customer

}

deinit { print("Карта #\(number) деинициализируется") }

}

Заметка

Свойство number класса CreditCard определено как значение типа UInt64, а не Int, для того, чтобы оно было достаточно большим, чтобы хранить числа с 16 цифрами и на 32, и на 64 разрядных системах.

Следующий кусок кода определяет опциональную переменную типа Customer? с именем john, которая будет использоваться для хранения ссылки на определенного клиента. Эта переменная имеет начальное значение nil, в силу того, что это опциональный тип:

var john: Customer?

Вы можете создать экземпляр Customer и использовать его для инициализации и присваивания нового экземпляра CreditCard, как свойство клиентской card:

john = Customer(name: "John Appleseed")

john!.card = CreditCard(number: 1234567890123456, customer: john!)

Вот как выглядят ссылки, после того как вы связали эти два экземпляра:

Экземпляр Customer имеет сильную ссылку на экземпляр CreditCard, а экземпляр CreditCard имеет бесхозную ссылку на экземпляр Customer.

Из-за того, что ссылка customer является бесхозной, то при разрушении сильной ссылки, которая находится в переменной john, больше сильных ссылок, указывающих на экземпляр Customer не остается:

Из-за того, что более сильных ссылок, ссылающихся на экземпляр Customer нет, то этот экземпляр освобождается. После того, как это происходит, у нас не остается больше сильных ссылок, указывающих на экземпляр CreditCard, так что он тоже освобождается:

john = nil

// Выведет "John Appleseed деинициализируется"

// Выведет "Карта #1234567890123456 деинициализируется"

Последний кусок кода показывает нам, что инициализаторы экземпляров Customer и CreditCard напечатали свои сообщения деинициализации, после того, как переменной john был присвоен nil.

Заметка

Примеры выше показывают как использовать safe unowned связи. Swift так же предоставляет unsafe unowned связи для случаев, где вам нужно отключить проверку безопасности во время исполнения, например в случае, когда вы хотите увеличить производительность. Как и со всеми небезопасными операциями, всю ответственность за проверку кода на безопасность вы берете на себя.

Чтобы показать, что вы будете использовать unsafe unowned связь, вам нужно написать unowned(unsafe). Если вы попытаетесь получить доступ к unsafe unowned ссылке после того, как экземпляр был освобожден, ваша программа попытается получить доступ к памяти, где этот объект хранился ранее, что само по себе является небезопасной операцией.

Бесхозные ссылки и неявно извлеченные опциональные свойства

Примеры, приведенные выше, для слабых и бесхозных ссылок, описывают два из самых распространенных сценариев, где существует необходимость разрушения цикла сильных ссылок.

Пример с Person, Apartment показывает ситуацию, где два свойства, оба из которых могут иметь значение nil, имеют потенциальную возможность образования цикла сильных связей. Этот случай лучше всего решается с помощью слабой связи.

Пример с Customer, CreditCard демонстрирует ситуацию, где одному свойству разрешено иметь значение nil, другому - нет. Однако здесь так же существует потенциальная возможность образования цикла сильных ссылок. Такой случай лучше всего разрешается с помощью бесхозных ссылок.

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

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

Пример внизу определяет два класса Country, City, каждый из которых хранит экземпляр другого класса в качестве свойства. В такой модели каждая страна должна иметь столицу, а каждый город, должен иметь страну. Для того, чтобы это отобразить, класс Country имеет свойство capitalCity, а класс City имеет свойство country:

class Country {

let name: String

var capitalCity: City!

init(name: String, capitalName: String) {

self.name = name

self.capitalCity = City(name: capitalName, country: self)

}

}

class City {

let name: String

unowned let country: Country

init(name: String, country: Country) {

self.name = name

self.country = country

}

}

Для создания такой внутренней зависимости между этими двумя классами, инициализатор City берет экземпляр Country и сохраняет его в свойство country.

Инициализатор City, вызывается из инициализатора Country. Однако инициализатор Country не может передавать self в инициализатор City до тех пор, пока новый экземпляр Country не будет полностью инициализирован, что описано в разделе “Двухфазная инициализация”.

Объединив все с этим требованием, вы объявляете свойство capitalCity класса Country как неявно извлеченное опциональное свойство, отображаемое восклицательным знаком в конце аннотации типа (City!). Это значит, что свойство capitalCity имеет начальное значение равное nil, как и в случае с другими опционалами, но к которому можно обратиться без предварительного развертывания значения, что описано в главе Неявно извлеченные опционалы.

Так как свойство capitalCity имеет значение по умолчанию nil, то новый экземпляр Country считается полностью инициализированным, как только экземпляр Country устанавливает свойство name с помощью своего инициализатора. Это значит, что инициализатор Country может ссылаться на неявное свойство self и раздавать его, как только свойство name получит корректное значение. Инициализатор Country может таким образом передать self в качестве одного из параметров для инициализатора City, когда инициализатор Country устанавливает свое собственное свойство capitalCity.

Из всего этого можно сделать вывод, что вы можете создать экземпляры Country и City единственным выражением, без создания цикла сильных ссылок друг на друга. Получить значение свойства capitalCity можно напрямую без использования восклицательного знака для извлечения опционального значения:

var country = Country(name: "Россия", capitalName: "Москва")

print("Столицей страны \(country.name) является \(country.capitalCity.name)")

// Выведет "Столицей страны Россия является Москва"

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

Циклы сильных ссылок в замыканиях

Как вы видели ранее, циклы сильных ссылок могут быть созданы двумя экземплярами классов, когда они поддерживают друг на друга сильные ссылки. Вы так же видели как использовать слабые (weak) или бесхозные (unowned) ссылки для того, чтобы заменить ими сильные (strong).

Сильные ссылки так же могут образовываться, когда вы присваиваете замыкание свойству экземпляра класса, и тело замыкания захватывает экземпляр. Этот захват может случиться из-за того, что тело замыкания получает доступ к свойству экземпляра, например self.someProperty, или из-за того, что замыкание вызывает метод типа self.someMethod(). В обоих случаях эти доступы и вызывают тот самый “захват” self, при этом создавая цикл сильных ссылок.

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

Swift предлагает элегантное решение этой проблемы, которые известно как список захвата замыкания (closure capture list). Однако до того, как вы узнаете, как разрушить такой цикл с помощью этого решения, давайте разберемся, что этот цикл может вызвать.

Пример ниже отображает, как вы можете создать цикл сильных ссылок, когда мы используем замыкание, которое ссылается на self. В этом примере определяем класс HTMLElement, который представляет модель простого элемента внутри HTML документа:

class HTMLElement {

let name: String

let text: String?

lazy var asHTML: () -> String = {

if let text = self.text {

return "<\(self.name)>\(text)</\(self.name)>"

} else {

return "<\(self.name) />"

}

}

init(name: String, text: String? = nil) {

self.name = name

self.text = text

}

deinit {

print("\(name) деинициализируется")

}

}

Класс HTMLElement определяет свойство name, которое отображает имя элемента, например "p" тег для отображения параграфа или “br” для тэга перехода на следующую строку. Класс HTMLElement также определяет опциональное свойство text, которому может быть присвоена строка, которая отображает текст, который может быть внутри HTML элемента.

В дополнение к этим двум простым свойствам класс HTMLElement определяет ленивое свойство asHTML. Это свойство ссылается на замыкание, которое комбинирует name, text во фрагмент HTML строки. Свойство asHTML имеет тип () -> String, или другими словами функция, которая не принимает параметров и возвращает строку.

По умолчанию свойству asHTML присвоено замыкание, которое возвращает строку, отображающую тэг HTML. Этот тэг содержит опциональный text, если таковой есть или не содержит его, если text, соответственно, отсутствует. Для элемента параграфа замыкание вернет “<p>some text</p>” или просто “<p />”, в зависимости от того, имеет ли свойство text какое либо значение или nil.

Свойство asHTML называется и используется несколько схоже с методом экземпляра. Однако из-за того что asHTML является свойством-замыканием, а не методом экземпляра, то вы можете заменить значение по умолчанию свойства asHTML на пользовательское замыкание, если вы хотите сменить отображение конкретного HTML элемента.

Например, свойству asHTML может быть присвоено замыкание, которое имеет дефолтный текст наслучай если свойство text равно nil, для предотвращения отображения пустого HTML тега:

let heading = HTMLElement(name: "h1")

let defaultText = "some default text"

heading.asHTML = {

return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"

}

print(heading.asHTML())

// Выведет "<h1>some default text</h1>"

Заметка

Свойство asHTML объявлено как ленивое свойство, потому что оно нам нужно только тогда, когда элемент должен быть отображен в виде строкового значения для какого-либо HTML элемента выходного значения. Факт того, что свойство asHTML является ленивым, означает, что вы можете ссылаться на self внутри дефолтного замыкания, потому что обращение к ленивому свойству невозможно до тех пор, пока инициализация полностью не закончится и не будет известно, что self уже существует.

Класс HTMLElement предоставляет единственный инициализатор, который принимает аргумент name и (если хочется) аргумент text для инициализации нового элемента. Класс также определяет деинициализатор, который выводит сообщение, для отображения момента когда экземпляр HTMLElement освобождается.

Вот как вы используете класс HTMLElement для создания и вывода нового экземпляра:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")

print(paragraph!.asHTML())

// Выведет "<p>hello, world</p>"

Заметка

Переменная paragraph определена как опциональный HTMLElement, так что он может быть и nil для демонстрации цикла сильных ссылок.

К сожалению класс HTMLElement, который описан выше, создает цикл сильных ссылок между экземпляром HTMLElement и замыканием, использованным для его исходного значения asHTML. Вот как выглядит этот цикл:

Свойство asHTML экземпляра держит сильную ссылку на его замыкание. Однако из-за того, что замыкание ссылается на self внутри своего тела (self.name, self.text), оно захватывает self, что означает, что замыкание держит сильную ссылку обратно на экземпляр HTMLElement. Между ними двумя образуется цикл сильных ссылок. (Для более подробной информации по захвату значений в замыканиях читайте соответствующий раздел Захват значений.)

Заметка

Даже несмотря на то, что замыкание ссылается на self несколько раз, оно захватывает лишь одну сильную ссылку на экземпляр HTMLElement.

Если вы установите значение paragraph на nil, чем разрушите сильную ссылку на экземпляр HTMLElement, то ни экземпляр HTMLElement, ни его замыкание не будут освобождены из-за цикла сильных ссылок:

paragraph = nil

Обратите внимание, что сообщение деинициализатора HTMLElement не выводится на экран, что и есть факт того, что этот экземпляр не освобожден.

Замена циклов сильных ссылок в замыканиях

Заменить цикл сильных ссылок между замыканием и экземпляром класса можно путем определения списка захвата в качестве части определения замыкания. Список захвата определяет правила, которые нужно использовать при захвате одного или более ссылочного типа в теле замыкания. Что же касательно циклов сильных связей между двумя экземплярами классов, то вы объявляете каждую захваченную ссылку как слабую или бесхозную (weak или unowned), вместо того, чтобы оставлять ее сильной (strong). Правильный выбор между слабой или бесхозной ссылками зависит от взаимоотношений между различными частями вашего кода.

Заметка

Swift требует от вас написания self.someProperty или self.someMethod() (вместо someProperty, someMethod()), каждый раз, когда вы обращаетесь к члену свойства self внутри замыкания. Это помогает вам не забыть, что возможен случай случайного захвата self.

Определение списка захвата

Каждый элемент в списке захвата является парой ключевого слова weak или unowned и ссылки на экземпляр класса (например, self) или переменная инициализированная с помощью какого-либо значения (например, delegate = self.delegate!). Эти пары вписываются в квадратные скобки и разделяются между собой запятыми.

Размещайте список захвата перед списком параметров замыкания и его возвращаемым типом:

lazy var someClosure: (Int, String) -> String = {

[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in

// тело замыкания

}

Если у замыкания нет списка параметров или возвращаемого типа, так как они могут быть выведены из контекста, то разместите список захвата в самом начале замыкания, перед словом in:

lazy var someClosure: () -> String = {

[unowned self, weak delegate = self.delegate!] in

// тело замыкания

}

Слабые (weak) или бесхозные (unowned) ссылки

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

Наоборот, определите список захвата в качестве слабой ссылки, когда захваченная ссылка может стать nil в какой-либо момент в будущем. Слабые ссылки всегда опционального типа и автоматически становятся nil, когда экземпляр, на который они ссылаются, освобождается. Это позволяет вам проверять их существование внутри тела замыкания.

Заметка

Если захваченная ссылка никогда не будет nil, то она должна быть всегда захвачена как unowned ссылка, а не weak ссылка.

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

class HTMLElement {

let name: String

let text: String?

lazy var asHTML: () -> String = {

[unowned self] in

if let text = self.text {

return "<\(self.name)>\(text)</\(self.name)>"

} else {

return "<\(self.name) />"

}

}

init(name: String, text: String? = nil) {

self.name = name

self.text = text

}

deinit {

print("\(name) освобождается")

}

}

Эта реализация HTMLElement идентична предыдущей реализации, кроме дополнения списка захвата внутри замыкания asHTML. В этом случае список захвата [unowned self], который означает: “захватить self как unowned ссылку, вместо strong”.

Вы можете создать и вывести экземпляр HTMLElement как и раньше:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")

print(paragraph!.asHTML())

// Выведет "<p>hello, world</p>"

Вот как теперь выглядят связи:

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

paragraph = nil

// Выведет "p освобождается"

Более подробную информацию можно найти в разделе "Список захвата".

Безопасность хранения

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

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

Что такое конфликт доступа к памяти

Доступ к памяти происходит в вашем коде, когда вы, например, устанавливаете значение переменной или передаете аргумент функции. Так следующий код содержит доступ и на чтение и на запись:

// Доступ к памяти с правами записи, где хранится данная переменная

var one = 1

// Доступ к памяти с правами чтения, где хранится данная переменная

print("We're number \(one)!”)

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

С такой же проблемой можно столкнуться, когда встает вопрос, как внести дополнительные траты в бюджет, который написан на листе бумаги. Обновление бюджета - это двухэтапный процесс: сначала вы добавляете названия и цены, а затем изменяете общую сумму, чтобы отразить элементы, находящиеся в списке. До и после обновления информация и конечная сумма являются корректными, как показано на рисунке ниже.

Но пока вы добавляете элементы в бюджет, он находится во временном, недействительном состоянии, потому что общая сумма еще не обновлена, и не отражает вновь добавленные элементы. И общая сумма в процессе добавления элементов дает нам неверную информацию.

Этот пример также отражает проблему, с которой вы можете столкнуться при устранении конфликта доступа к памяти: иногда есть несколько способов исправить конфликт, порождающий разные ответы, и не всегда очевидно, какой ответ правильный. В этом примере, в зависимости от того, хотите ли вы получить исходную общую текущую сумму или обновленную общую сумму, правильный ответ может быть равен $5 или $320. Прежде чем вы сможете исправить конфликт доступа, вы должны определить, в чем именно он заключается.

Заметка

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

Если у вас есть конфликт доступа к памяти в одном потоке, Swift гарантирует, что вы получите сообщение об ошибке во время компиляции или выполнения. Для многопоточного кода используйте Санитайзер потоков, чтобы выявить конфликт доступа по потокам.

Характеристики доступа к памяти

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

По крайней мере, один из них является доступом на запись.

Они получают доступ к одному и тому же фрагменту в памяти.

Их длительность перекрывается друг другом.

Разница между доступом для чтения и доступом для записи очевидна: доступ на запись изменяет местоположение в памяти, а доступ для чтения нет. Местоположение в памяти относится к тому, к чему обращаются, например, к переменной, константе или свойству. Длительность доступа к памяти - мгновенная или долгосрочная.

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

func oneMore(than number: Int) -> Int {

return number + 1

}

var myNumber = 1

myNumber = oneMore(than: myNumber)

print(myNumber)

// Выведет "2"

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

Перекрывающиеся обращения появляются, прежде всего, в коде, который использует сквозные параметры в функциях и методах или методы структуры c модификатором mutating. Конкретные примеры кода в Swift, использующие долгосрочные доступы, будут рассмотрены в следующих разделах.

Конфликт доступа к сквозным параметрам

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

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

var stepSize = 1

func incrementInPlace(_ number: inout Int) {

number += stepSize

}

incrementInPlace(&stepSize)

// Ошибка: conflicting accesses to stepSize

В приведенном выше коде stepSize является глобальной переменной, и она обычно доступна из incrementInPlace(_:). Тем не менее, доступ для чтения к stepSize перекрывается доступом для записи к number. Как показано на рисунке ниже, и number, и stepSize относятся к одному и тому же фрагменту памяти. Доступ для чтения и записи относится к одной и той же памяти, и они перекрываются, создавая конфликт.

Один из способов решения этого конфликта - сделать явную копию stepSize:

// Make an explicit copy.

var copyOfStepSize = stepSize

incrementInPlace(&copyOfStepSize)

// Update the original.

stepSize = copyOfStepSize

// stepSize равен 2

// stepSize равен 2

Когда вы создаете копию stepSize перед вызовом incrementInPlace(_:) то становится ясно, что значение copyOfStepSize увеличивается на текущий размер шага. Доступ для чтения заканчивается до начала доступа для записи, поэтому конфликт не возникает.

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

func balance(_ x: inout Int, _ y: inout Int) {

let sum = x + y

x = sum / 2

y = sum - x

}

var playerOneScore = 42

var playerTwoScore = 30

balance(&playerOneScore, &playerTwoScore) // OK

balance(&playerOneScore, &playerOneScore)

// Ошибка: Conflicting accesses to playerOneScore

Функция balance(_:_:) изменяет два своих параметра, чтобы равномерно распределять общее значение между ними. Вызов с помощью playerOneScore и playerTwoScore в качестве аргументов не приводит к конфликту - есть два доступа для записи, которые перекрываются во времени, но они получают доступ к различным фрагментам в памяти. Напротив, передача playerOneScore в качестве значения для обоих параметров вызывает конфликт, поскольку он пытается одновременно выполнить два доступа для записи в одно и то же место в памяти.

Заметка

Так как операторы это функции, то они также могут иметь долгосрочный доступ к своим сквозным параметрам. Например, если balance(_:_:) это операторная функция с именем <^>, то запись playerOneScore <^> playerOneScore приведет к такому же конфликту, что и balance(&playerOneScore, &playerOneScore)

Конфликт доступа к self в методах

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

struct Player {

var name: String

var health: Int

var energy: Int

static let maxHealth = 10

mutating func restoreHealth() {

health = Player.maxHealth

}

}

В вышеописанном методе restoreHealth() доступ для записи к self начинается с начала метода и продолжается до тех пор, пока метод не вернется. В этом случае внутри restoreHealth() нет другого кода, который может иметь перекрывающий доступ к свойствам экземпляра Player. Метод shareHealth(with:) (см. ниже) принимает другой экземпляр Player как сквозной параметр, создавая возможность перекрытия доступа.

extension Player {

mutating func shareHealth(with teammate: inout Player) {

balance(&teammate.health, &health)

}

}

var oscar = Player(name: "Oscar", health: 10, energy: 10)

var maria = Player(name: "Maria", health: 5, energy: 10)

oscar.shareHealth(with: &maria) // OK

В приведенном выше примере вызов shareHealth(with:) для игрока Оскара для совместного использования здоровья с игроком Марией не вызывает конфликт. Во время вызова метода есть доступ для записи и к oscar, потому что oscar - это значение self в изменяющем методе, и есть доступ на запись к maria в течение той же продолжительности, потому что maria была передана как сквозной параметр. Как показано на рисунке ниже, они получают доступ к различным фрагментам памяти. Несмотря на то, что два доступа для записи перекрываются во времени, они не конфликтуют.

Однако, если вы передадите oscar в качестве аргумента shareHealth(with:), то возникает конфликт:

oscar.shareHealth(with: &oscar)

// Ошибка: conflicting accesses to oscar

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

Конфликт доступа к свойствам

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

var playerInformation = (health: 10, energy: 20)

balance(&playerInformation.health, &playerInformation.energy)

// Ошибка: conflicting access to properties of playerInformation

В приведенном выше примере вызов balance(_:_:) по элементам кортежа создает конфликт, потому что есть перекрывающий доступ для записи к playerInformation. И playerInformation.health, и playerInformation.energy передаются как сквозные параметры, что означает, что balance(_:_:) требует доступ для записи во время вызова функции. В обоих случаях доступ для записи к элементу кортежа требует доступа для записи ко всему кортежу. Это означает, что есть два доступа для записи в playerInformation с длительностью, которая перекрывается, вызывая конфликт.

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

var holly = Player(name: "Holly", health: 10, energy: 10)

balance(&holly.health, &holly.energy) // Ошибка

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

func someFunction() {

var oscar = Player(name: "Oscar", health: 10, energy: 10)

balance(&oscar.health, &oscar.energy) // OK

}

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

Ограничение перекрывающегося доступа к свойствам структуры не всегда необходимо для сохранения безопасности хранения. Безопасность хранения - это желаемая гарантия, но эксклюзивный доступ является более строгим требованием, чем безопасность хранения, что означает, что код может сохранять безопасность хранения, даже несмотря на то, что он нарушает исключительный доступ к памяти. Swift позволяет использовать этот безопасный для памяти код, если компилятор может доказать, что неисключительный доступ к памяти по-прежнему безопасен. В частности, он может доказать, что перекрывающий доступ к свойствам структуры безопасен, если применяются следующие условия:

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

Структура - это значение локальной переменной, а не глобальной переменной.

Структура либо не захватывается никакими замыканиями, либо захватывается только несбегающими замыканиями.

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

Контроль доступа

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

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

В дополнение к предложению контроля различных уровней доступа, 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.

Заметка

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

Продвинутые операторы

В дополнение к операторам, которые мы рассматривали в главе Базовые операторы, Swift предоставляет нам еще несколько продвинутых операторов, которые позволяют нам проводить более сложные манипуляции со значениями. Они включают в себя побитовые и операторы разрядного смещения, с которыми вы возможно знакомы из языков C или Objective-C.

В отличии от арифметических операторов C, арифметические операторы в Swift не переполняются по умолчанию. Переполнения отслеживаются и выводятся как ошибка. Для того, чтобы этого избежать, вы можете использовать оператор из второго набора арифметических операторов Swift (&+). Все операторы переполнения начинаются с амперсанда (&).

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

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

Побитовые операторы

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

Swift поддерживает все побитовые операторы, которые были основаны в C о которых мы поговорим далее.

Побитовый оператор NOT

Побитовый оператор NOT (~) инвертирует все битовые числа:

Побитовый оператор NOT является префиксным оператором и ставится прямо перед значением (без пробела), над которым он оперирует.

let initialBits: UInt8 = 0b00001111

let invertedBits = ~initialBits // равен 11110000

Целые числа типа UInt8 имеют восемь бит и могут хранить значения от 0 до 255. В этом примере инициализируем число типа UInt8, которое имеет бинарное значение 00001111, которое имеет первые четыре бита равные 0, а вторая четверка битов равна 1. Это эквивалент числа 15.

Далее используем побитовый оператор NOT для создания новой константы invertedBits, которая равна initialBits, но только с перевернутыми битами. То есть теперь все единицы стали нулями, а нули единицами. Значение числа invertedBits равно 11110000, что является эквивалентом 240.

Побитовый оператор AND

Побитовый оператор AND (&) комбинирует два бита двух чисел. Он возвращает новое число, чье значение битов равно 1, если только оба бита из входящих чисел были равны 1:

В примере ниже, значения firstSixBits и lastSixBits имеют четыре бита по середине равными 1. Побитовый оператор AND комбинирует их для создания числа 00111100, которое равно беззнаковому целому числу 60:

let firstSixBits: UInt8 = 0b11111100

let lastSixBits: UInt8 = 0b00111111

let middleFourBits = firstSixBits & lastSixBits // равен 00111100

Побитовый оператор OR

Побитовый оператор OR (|) сравнивает биты двух чисел. Оператор возвращает новое число, чьи биты устанавливаются на 1, если один и пары битов этих двух чисел имеет бит равный 1:

В примере ниже значения someBits и moreBits имеют разные биты со значениями 1. Побитовый оператор OR комбинирует их для создания числа 11111110, что равно беззнаковому целому числу 254:

let someBits: UInt8 = 0b10110010

let moreBits: UInt8 = 0b01011110

let combinedbits = someBits | moreBits // равен 11111110

Побитовый оператор XOR

Побитовый оператор XOR или “оператор исключающего OR” (^), который сравнивает биты двух чисел. Оператор возвращает число, которое имеет биты равные 1, когда биты входных чисел разные, и возвращает 0, когда биты одинаковые:

В примере ниже, значения firstBits и otherBits каждый имеет один бит в том месте, где у другого 0. Побитовый оператор XOR устанавливает оба этих бита в качестве выходного значения. Все остальные биты повторяются, поэтому оператор возвращает 0:

let firstBits: UInt8 = 0b00010100

let otherBits: UInt8 = 0b00000101

let outputBits = firstBits ^ otherBits // равен 00010001

Операторы побитового левого и правого сдвига

Оператор побитового левого сдвига (<<) и оператор побитового правого сдвига (>>) двигают все биты числа влево или вправо на определенное количество мест, в зависимости от правил, которые определены ниже.

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

Поведение сдвига для беззнаковых целых чисел

Поведение побитового сдвига имеет следующие правила:

Существующие биты сдвигаются направо или налево на требуемое число позиций.

Любые биты, которые вышли за границы числа, отбрасываются.

На пустующие позиции сдвинутых битов вставляются нули.

Такой подход называется логическим сдвигом.

Иллюстрация внизу отображает результат смещения 11111111 << 1 (что означает 11111111 сдвинутые влево на 1), и 11111111 >> 1 (что означает 11111111 сдвинутые на 1 вправо). Голубые цифры - сдвинутые, серые - отброшенные, оранжевые - вставленные:

Вот как выглядит побитовый сдвиг в виде Swift кода:

let shiftBits: UInt8 = 4 // 00000100 бинарный вид

shiftBits << 1 // 00001000

shiftBits << 2 // 00010000

shiftBits << 5 // 10000000

shiftBits << 6 // 00000000

shiftBits >> 2 // 00000001

Вы можете использовать побитовый сдвиг для шифрования и дешифрования значений внутри других типов данных:

let pink: UInt32 = 0xCC6699

let redComponent = (pink & 0xFF0000) >> 16 // redComponent равен 0xCC, или 204

let greenComponent = (pink & 0x00FF00) >> 8 // greenComponent равен 0x66, или 102

let blueComponent = pink & 0x0000FF // blueComponent равен 0x99, или 153

Этот пример использует UInt32, который называется pink, для хранения значения розового цвета из файла CSS. Значение розового цвета #CC6699, что записывается в виде шестнадцатеричном представлении Swift как 0xCC6699. Этот цвет затем раскладывается на его красный(CC), зеленый (66) и голубой (99) компоненты при помощи побитового оператора AND (&) и побитового оператора правого сдвига (>>).

Красный компонент получен с помощью побитового оператора AND между числами 0xCC6699 и 0xFF0000. Нули в 0xFF0000 фактически являются “маской” для третьего и четвертого бита в 0xCC6699, тем самым заставляя игнорировать 6699, и оставляя 0xCC0000 в качестве результата.

После этого число сдвигается на 16 позиций вправо (>> 16). Каждая пара символов в шестнадцатеричном числе использует 8 битов, так что сдвиг вправо на 16 позиций преобразует число 0xCC0000 в 0x0000CC. Это то же самое, что и 0xCC, которое имеет целое значение равное 204.

Аналогично с зеленым компонентом, который получается путем использования побитового оператора AND между числами 0xCC6699 и 0x00FF00, который в свою очередь дает нам выходное значение 0x006600. Это выходное значение затем сдвигается на восемь позиций вправо, давая нам значение 0x66, что имеет целое значение равное 102.

Ну а теперь последний синий компонент, который получается при использовании побитового оператора AND между числами 0xCC6699 и 0x0000FF, что в свою очередь дает нам выходное значение равное 0x000099. Таким образом, нам не нужно сдвигать это вправо, так как 0x000099 уже равно 0x99, что имеет целое значение равное 153.

Поведение побитового сдвига для знаковых целых чисел

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

Знаковые целые числа используют первый бит (известный как знаковый бит) для индикации того, является ли число положительным или отрицательным. Значение знакового бита равное 0 свидетельствует о положительном числе, 1 - отрицательном.

Остальные биты (известные как биты значения) хранят фактическое значение. Положительные числа хранятся в точности так же как и беззнаковые целые числа, считая от 0. Вот как выглядят биты внутри Int8 для числа 4:

Знаковый бит равен 0 (число положительное), остальные семь битов означают число 4, записанное в бинарной форме.

Однако отрицательные числа хранятся иначе. Они хранятся путем вычитания их абсолютного значения из 2 в степени n, где n - количество битов значения.

Вот как выглядит биты внутри Int8 для числа -4:

В этот раз, знаковый бит равен 1 (число отрицательное), а остальные семь знаковых бита имеют бинарное значение числа 124 (что означает 128 - 4):

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

Первое. Вы можете добавить -1 к -4, просто выполняя стандартное сложение всех восьми битов (включая и восьмой бит), и отбрасывая все, что не поместится в ваши восемь бит:

Второе. Представление “дополнения до 2” так же позволяет вам сдвигать биты отрицательных чисел влево и вправо, как в случае с положительными, и все так же умножая их при сдвиге влево или уменьшая их в два раза, при сдвиге на 1 место вправо. Для того чтобы обеспечить такое поведение при движении знаковых чисел вправо, мы должны применить дополнительное правило: Когда вы сдвигаете знаковое число вправо, используйте тоже самое правило, что и для беззнаковых чисел, но заполняйте освободившиеся левые биты знаковыми битами, а не нулями.

Эти действия гарантируют вам, что знаковые числа имеют тот же знак, после того как они сдвинуты вправо, и эти действия известны как арифметический сдвиг.

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

Операторы переполнения

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

Для примера, целочисленный тип Int16 может держать любое знаковое целое число от -32768 и до 32767. Если вы попытаетесь установить число (константу или переменную) типа Int16 за границами приведенного диапазона, то вы получите ошибку:

var potentialOverflow = Int16.max

// potentialOverflow равняется 32767, что является самым большим значением, которое может содержаться в Int16

potentialOverflow += 1

// это вызовет ошибку

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

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

Оператор переполнения с добавлением (&+)

Оператор переполнения с вычитанием (&-)

Оператор переполнения с умножением (&*)

Переполнение значения

Числа могут переполняться как в положительную, так и в отрицательную сторону.

Ниже приведен пример того, что случится, когда беззнаковое значение позволяет переполнить себя, с использованием оператора (&+):

var willOverflow = UInt8.max

// willOverflow равняется 255, что является наибольшим числом, которое может держать UInt

willOverflow = willOverflow &+ 1

// willOverflow теперь равно 0

Переменная willOverflow инициализирована самым большим числом, которое может держать UInt8 (255 или в бинарном виде 11111111). Затем оно увеличивается на 1 при помощи оператора переполнения (&+). Это выталкивает бинарное представление размерности UInt8, вызывая тем самым переполнение границ, что отображено на диаграмме ниже. Значение, которое остается в пределах границ значения типа UInt8 после переполнения и добавления выглядит как 00000000, или попросту 0 в десятичной форме:

Числа так же могут быть слишком маленькими, чтобы соответствовать определенному типу. Ниже приведен пример с использованием оператора недополнения (&-)

var unsignedOverflow = UInt8.min

// unsignedOverflow равен 0, что является наименьшим возможным значением UInt8

unsignedOverflow = unsignedOverflow &- 1

// unsignedOverflow теперь равно 255

Самое маленькое значение, которое может держать UInt8 равно 0 (что отображается как 00000000 в восьмибитной бинарной форме). Если вы из 00000000 вычтите 1, с использованием оператора недополнения, число переполнится в обратную сторону к 11111111, или к 255 в десятичной форме:

Аналогичное недополнение случается и с знаковыми целыми числами. Все вычитание для знаковых целых чисел проводится как прямое бинарное вычитание с учетом знакового бита, в качестве части вычитаемых чисел, что описано в Операторы побитового левого и правого сдвига.

var signedUnderflow = Int8.min

// signedUnderflow равняется -128, что является самым маленьким числом, которое может держать Int8

signedUnderflow = signedUnderflow &- 1

// signedUnderflow теперь равняется 127

Самым маленьким числом, которое может держать Int8, является –128, что записывается в бинарной форме как 10000000. Вычитая 1 из этого бинарного числа с оператором недополнения, дает нам значение 01111111, что переключает наш знаковый бит на противоположный и дает нам положительное 127, что является самым большим числом, которое может держать Int8:

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

Приоритет и ассоциативность

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

Оператор ассоциативности определяет то, как операторы одного приоритета сгруппированы вместе (или ассоциированы друг с другом), то есть либо они сгруппированы слева, либо справа. Думайте об этом как “они связаны с выражением слева” или “они связаны с выражением справа”.

Это важно учитывать приоритет и ассоциативность каждого оператора, когда работаете с порядком, в котором должно считаться выражение. Вот простой пример. Почему данное выражение имеет равенство 17?

2 + 3 % 4 * 5

// это равно 17

Если вы прочитаете это выражение строго слева направо, то вы можете ожидать действия в таком порядке:

2 плюс 3 равняется 5

5 остаток от деления на 4 равен 1

1 умножаем на 5 и получаем 5

Однако, как не крути, правильный ответ равен 17, а не 5. Операторы более высокого приоритета выполняются раньше операторов более низкого приоритета. В Swift, как и в C, оператор умножения (*) и оператор остатка (%) имеют более высокий приоритет, чем оператор сложения (+). В результате они оба вычисляются раньше, чем вычисляется оператор сложения.

Однако оператор умножения и оператор остатка имеют один и тот же приоритет по отношению друг к другу. Для выяснения точного порядка вычисления вы должны обратиться к их ассоциативности. Операторы умножения и остатка оба ассоциируются с выражением слева от себя. Представляйте это, как будто вы добавили скобки вокруг этих частей выражения, начиная слева:

2 + ((3 % 4) * 5)

(3 % 4) равно 3, значит можно записать:

2 + (3 * 5)

(3 * 5) равно 15, и значит мы можем записать:

2 + 15

Таким образом наш конечный результат равен 17.

Для получения информации об операторах, предоставляемых стандартной библиотекой Swift, включая полный список групп приоритета операторов и параметров ассоциативности, см. Объявление операторов.

Заметка

Правила приоритета и ассоциативности операторов Swift проще и более предсказуемые чем в C или Objective-C. Однако это означает, что они ведут себя не так же как они вели себя в этих C-языках. Будьте внимательны с тем, как ведут себя операторы взаимодействия при переносе кода в Swift.

Операторные функции

Классы и структуры могут предоставлять свои собственные реализации существующих операторов. Действие переопределения оператора известно как перегрузка существующего оператора.

Пример ниже отображает как можно реализовать арифметический оператор сложения (+) для пользовательской структуры. Арифметический оператор сложения является бинарным оператором, потому что он оперирует с двумя операндами, то есть он является инфиксным, потому как вставляется между двумя операндами.

Пример определяет структуру Vector2D для двухмерного вектора положения (x, y), за которой идет статическая функция, которая добавляет друг к другу экземпляры структуры Vector2D:

struct Vector2D {

var x = 0.0, y = 0.0

}

extension Vector2D {

static func + (left: Vector2D, right: Vector2D) -> Vector2D {

return Vector2D(x: left.x + right.x, y: left.y + right.y)

}

}

Операторный метод определен как метод типа Vector2D с именем метода, которое совпадает с именем оператора, который перегружают (+). Так как сложение не является неотъемлемой частью поведения вектора, метод типа определен в расширении нашего Vector2D, а не в основной структуре Vector2D. Так как арифметический оператор сложения является бинарным оператором, то этот оператор принимает два параметра типа Vector2D и возвращает единственное выходное значение, которое тоже имеет тип Vector2D.

В этой реализации входные параметры имеют имена left и right, для отображения экземпляров Vector2D, которые будут по левую и по правую сторону от оператора +. Функция возвращает новый экземпляр Vector2D, x и y которого инициализированы суммой свойств x и y из двух экземпляров Vector2D, которые были добавлены друг другу.

Метод типа может использоваться как инфиксный оператор между существующими экземплярами Vector2D:

let vector = Vector2D(x: 3.0, y: 1.0)

let anotherVector = Vector2D(x: 2.0, y: 4.0)

let combinedVector = vector + anotherVector

// combinedVector является экземпляром Vector2D, который имеет значения (5.0, 5.0)

Этот пример складывает два вектора вместе (3.0, 1.0) и (2.0, 4.0) для создания вектора (5.0, 5.0), который нарисован ниже:

Префиксные и постфиксные операторы

Пример, отображенный выше, демонстрирует пользовательскую реализацию бинарного инфиксного оператора. Классы и структуры так же могут обеспечивать реализацию стандартных унарных операторов. Унарные операторы работают с одним операндом. Они бывают префиксными, если они предшествуют их операнду (например, -a) или постфиксными, если они следуют за операндом (например b!).

Вы реализуете префиксный или постфиксный унарный оператор при помощи модификаторов prefix или postfix перед ключевым словом func, когда объявляете операторную функцию:

extension Vector2D {

static prefix func - (vector: Vector2D) -> Vector2D {

return Vector2D(x: -vector.x, y: -vector.y)

}

}

Пример выше реализует унарный оператор (-a) для экземпляров Vector2D. Оператор унарного минуса является префиксным оператором, таким образом эта функция должна быть модифицирована при помощи prefix модификатора.

Для простых числовых значений оператор унарного минуса конвертирует положительные числа в их негативный эквивалент и наоборот. Соответствующая реализация для экземпляров Vector2D проводит операции и на x, и на y свойствах:

let positive = Vector2D(x: 3.0, y: 4.0)

let negative = -positive

// negative - экземпляр Vector2D со значениями (-3.0, -4.0)

let alsoPositive = -negative

// alsoPositive - экземпляр Vector2D со значениями (3.0, 4.0)

Составные операторы присваивания

Составные операторы присваивания комбинируют оператор присваивания (=) с другим оператором. Например, оператор сложения-присваивания (+=) комбинирует в себе оператор добавления и оператор присваивания. Вы обозначаете левый входной параметр составного оператора как inout, потому что именно эта величина и будет изменена напрямую изнутри самой операторной функции.

Пример ниже реализует операторную функцию добавления-присваивания для экземпляров Vector2D:

extension Vector2D {

static func += (left: inout Vector2D, right: Vector2D) {

left = left + right

}

}

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

var original = Vector2D(x: 1.0, y: 2.0)

let vectorToAdd = Vector2D(x: 3.0, y: 4.0)

original += vectorToAdd

// original теперь имеет значения (4.0, 6.0)

Заметка

Нет такой возможности перегрузить оператор присваивания (=). Только составные операторы могут быть перегружены. Тернарный оператор (a ? b : c) так же не может быть перегружен.

Операторы эквивалентности

Пользовательские классы и структуры не получают дефолтной реализации эквивалентных операторов, известных как “равен чему-то” оператор (==) или “не равен чему-то” (!=).

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

extension Vector2D: Equatable {

static func == (left: Vector2D, right: Vector2D) -> Bool {

return (left.x == right.x) && (left.y == right.y)

}

}

Пример выше реализует оператор “равен чему-то” (==) для проверки эквивалентности значений двух экземпляров Vector2D. В контексте Vector2D имеет смысл считать, что “равно чему-то” означает, что “оба экземпляра имеют одни и те же значения x и y”, таким образом это является той логикой, которая используется при реализации оператора. Пример так же реализует оператор “не равен чему-то” (!=), который просто возвращает обратный результат оператора “равен чему-то”.

Теперь вы можете использовать эти операторы для проверки того, эквивалентны ли экземпляры Vector2D друг другу или нет:

let twoThree = Vector2D(x: 2.0, y: 3.0)

let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)

if twoThree == anotherTwoThree {

print("Эти два вектора эквиваленты.")

}

// Выведет "Эти два вектора эквиваленты."

Swift предоставляет синтезированные реализации операторов эквивалентности для следующих пользовательских типов:

Структур, имеющих только свойства хранения, соответствующие протоколу Equatable

Перечислений, имеющих только ассоциированные типы, соответствующие протоколу Equatable

Перечислений, не имеющих связанных типов

Объявите о соответствии протоколу Equatable в исходной  реализации для получения этих дефолтных реализаций.

Приведенный ниже пример определяет структуру Vector3D для трехмерного вектора положения (x, y, z), аналогичную структуре Vector2D. Поскольку свойства x, y и z являются эквивалентными, Vector3D принимает стандартные реализации операторов эквивалентности.

struct Vector3D: Equatable {

var x = 0.0, y = 0.0, z = 0.0

}

let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)

let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)

if twoThreeFour == anotherTwoThreeFour {

print("These two vectors are also equivalent.")

}

// Выведет "These two vectors are also equivalent."

Пользовательские операторы

Вы можете объявить и реализовать ваши собственные пользовательские операторы в дополнение к стандартным операторам Swift. Список символов, которые можно использовать для определения пользовательских операторов, см. в разделе «Операторы».

Новые операторы объявляются на глобальном уровне при помощи ключевого слова operator и отмечаются модификатором prefix, infix, postfix:

prefix operator +++

Пример выше определяет новый префиксный оператор +++. Этот оператор не имеет никакого значения в Swift, таким образом мы даем ему собственное назначение, которое описано чуть ниже, которое имеет специфический контекст работы с экземплярами Vector2D. Для целей этого примера, оператор +++ рассматривается как новый “префиксный двойной” оператор. Он удваивает значения x и y экземпляра Vector2D, путем добавления вектора самому себе при помощи оператора сложения-присваивания, который мы определили ранее. Для реализации +++ оператора мы добавим метод типа Vector2D с именем +++ как показано ниже:

extension Vector2D {

static prefix func +++ (vector: inout Vector2D) -> Vector2D {

vector += vector

return vector

}

}

var toBeDoubled = Vector2D(x: 1.0, y: 4.0)

let afterDoubling = +++toBeDoubled

// toBeDoubled имеет значения (1.0, 4.0)

// afterDoubling так же имеет значения (2.0, 8.0)

Приоритет для пользовательских инфиксных операторов

Каждый пользовательский infix оператор принадлежит к своей приоритетной группе. Группа приоритета определяет приоритет оператора по отношению к другим инфиксным опреторам, так же как и ассоциативность оператора. Посмотрите главу Приоритет и ассоциативность для объяснения того, как эти две характеристики влияют на взаимодействие инфиксных операторов между собой.

Пользовательскому инфиксному оператору, который явно не размещен в приоритетной группе, предоставляется дефолтная группа приоритета, которая является по приоритету следующей после тернарного условного оператора.

Следующий пример определяет новый инфиксный оператор +- левой ассоциативности и с приоритетом AdditionPrecedence:

infix operator +-: AdditionPrecedence

extension Vector2D {

static func +- (left: Vector2D, right: Vector2D) -> Vector2D {

return Vector2D(x: left.x + right.x, y: left.y - right.y)

}

}

let firstVector = Vector2D(x: 1.0, y: 2.0)

let secondVector = Vector2D(x: 3.0, y: 4.0)

let plusMinusVector = firstVector +- secondVector

// plusMinusVector является экземпляром Vector2D со значениями (4.0, -2.0)

Этот оператор складывает значения x двух векторов и вычитает значение y второго вектора из значения y первого вектора. Так как этот оператор в сущности является оператором “сложения”, то приоритет будет равным операторам сложения (+) и вычитания (-). Для получения информации об операторах, представленных стандартной библиотекой Swift, включая список настроек ассоциативности и групп приоритета смотрите раздел "Объявления операторов". Для более полной информации по группам приоритета и для ознакомления с синтаксисом создания своих собственных операторов и групп ассоциативности смотрите "Объявление оператора".

Заметка

Вы не указываете приоритет, когда определяете префиксный и постфиксные операторы. Однако, если вы воздействуете на операнд сразу двумя операторами (префиксным и постфиксным), то первым будет применен постфиксный оператор.

Справочник языка

Этот раздел книги описывает формальную грамматику языка программирования Swift. Грамматика, описанная здесь, предназначена скорее для того, чтобы помочь вам более подробно понять язык, а не просто прямолинейно реализовывать анализатор или компилятор.

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

Как читать Грамматику

Для описания формальной грамматики языка программирования Swift используются следующие условные обозначения:

Стрелка (→) используется для обозначения грамматических производных и может быть прочитана как "может состоять из".

Синтаксические категории обозначены курсивом и расположены по обеим сторонам правила грамматической производной.

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

Альтернативные грамматические производные разделяются вертикальными чертами (|). Когда альтернативные производные слишком длинные для того, чтобы их было легко читать, они разбиваются на несколько правил грамматической производной в новых строках.

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

Дополнительные синтаксические категории и литералы обозначаются нижним индексом, opt.

В качестве примера, грамматика блока геттера-сеттера будет выглядеть следующим образом:

Грамматика блока геттера-сеттера

getter-setter-block → {getter-clause setter-clauseopt} | {setter-clause getter-clause}

Это определение указывает на то, что блок геттера-сеттера может состоять из getter clause, за которым следует опциональный setter clause, заключенный в скобки, или setter clause, за которым следует getter clause в скобках. Приведенное выше правило эквивалентно следующим двум производным, где альтернативы явно прописаны:

Грамматика блока геттера-сеттера

getter-setter-block → {getter-clause setter-clauseopt}

getter-setter-block → {setter-clause getter-clause}

Лексическая структура

Лексическая структура Swift описывает какая последовательность символов образует валидные токены языка. Эти токены образуют строительные блоки низшего уровня языка и используются для описания остальной части языка в последующих главах. Токен состоит из идентификатора, ключевого слова, пунктуации, литерала или оператора.

В большинстве случаев, токены генерируются из символов исходного файла Swift с учетом максимально длинной подстроки вводимого текста, с грамматическими ограничениями, описанными ниже. Такое поведение называется longest match или maximal munch.

Пробелы и комментарии

Пробелы используются для двух целей: для разделения токенов в исходном файле и для того, чтобы помочь определить является ли оператор префиксом или постфиксом (см. раздел "Операторы" ниже), в остальных случаях их использование игнорируется. Следующие символы считаются пробелами: промежуток (U + 0020), перевод строки (U + 000A), возврат каретки (U + 000D), горизонтальная вкладка (U + 0009), вертикальная вкладка (U + 000B), смена страницы (очистка экрана)(U + 000C) и нуль (U + 0000).

Комментарии обрабатываются как пробелы компилятором. Однострочные комментарии начинаются с // и продолжаются до расстояния между строкой (U + 000A) или возврата каретки (U + 000D). Многострочные комментарии начинаются с / * и заканчиваются */. Вложение многострочных комментариев допускается, но маркеры комментариев должны быть сбалансированы.

Комментарии могут содержать дополнительное форматирование и разметку, как описано в Markup Formatting Reference.

Грамматика пробелов

whitespace → whitespace-item whitespaceopt

whitespace-item → line-break

whitespace-item → comment

whitespace-item → multiline-comment

whitespace-item → U+0000, U+0009, U+000B, U+000C, or U+0020

line-break → U+000A

line-break → U+000D

line-break → U+000D followed by U+000A

comment → // comment-text line-break

multiline-comment → /* multiline-comment-text */

comment-text → comment-text-item comment-textopt

comment-text-item → Any Unicode scalar value except U+000A or U+000D

multiline-comment-text→multiline-comment-text-item multiline-comment-textopt

multiline-comment-text-item → multiline-comment

multiline-comment-text-item → comment-text-item

multiline-comment-text-item → Any Unicode scalar value except /* or */

Идентификаторы

Идентификаторы начинаются с заглавной или строчной буквы от А до Z, знак подчеркивания (_), некомбинируемого алфавитно-цифрового символа Unicode в базовом многоязычном разделе, или символа вне базового многоязычного раздела, который не находится в зоне личного пользования. После первого символа, цифры и комбинируемые символы Unicode также допускаются.

Чтобы использовать зарезервированное слово в качестве идентификатора, поставьте обратные одиночные кавычки (`) до и после него. Например, class не является допустимым идентификатором, но `class` действителен. Открывающиеся кавычки не считаются частью идентификатора, `x` и x имеют одинаковое значение.

Внутри замыкания, где имена параметров не указываются явно, можно использовать неявные имена параметров: $0, $1, $2 и так далее. Эти имена являются допустимыми идентификаторами в пределах замыкания.

Грамматика идентификатора

identifier → identifier-head identifier-charactersopt

identifier → `identifier-head identifier-charactersopt`

identifier → implicit-parameter-name

identifier-list → identifier identifier,identifier-list

identifier-head → Upper- or lowercase letter A through Z

identifier-head → _

identifier-head → U+00A8, U+00AA, U+00AD, U+00AF, U+00B2–U+00B5, or U+00B7–U+00BA

identifier-head → U+00BC–U+00BE, U+00C0–U+00D6, U+00D8–U+00F6, or U+00F8–U+00FF

identifier-head → U+0100–U+02FF, U+0370–U+167F, U+1681–U+180D, or U+180F–U+1DBF

identifier-head → U+1E00–U+1FFF

identifier-head → U+200B–U+200D, U+202A–U+202E, U+203F–U+2040, U+2054, or U+2060–U+206F

identifier-head → U+2070–U+20CF, U+2100–U+218F, U+2460–U+24FF, or U+2776–U+2793

identifier-head → U+2C00–U+2DFF or U+2E80–U+2FFF

identifier-head → U+3004–U+3007, U+3021–U+302F, U+3031–U+303F, or U+3040–U+D7FF

identifier-head → U+F900–U+FD3D, U+FD40–U+FDCF, U+FDF0–U+FE1F, or U+FE30–U+FE44

identifier-head → U+FE47–U+FFFD

identifier-head → U+10000–U+1FFFD, U+20000–U+2FFFD, U+30000–U+3FFFD, or U+40000–U+4FFFD

identifier-head → U+50000–U+5FFFD, U+60000–U+6FFFD, U+70000–U+7FFFD, or U+80000–U+8FFFD

identifier-head → U+90000–U+9FFFD, U+A0000–U+AFFFD, U+B0000–U+BFFFD, or U+C0000–U+CFFFD

identifier-head → U+D0000–U+DFFFD or U+E0000–U+EFFFD

identifier-character → Цифры от 0 до 9 (включительно)

identifier-character → U+0300–U+036F, U+1DC0–U+1DFF, U+20D0–U+20FF, or U+FE20–U+FE2F

identifier-character → identifier-head

identifier-characters → identifier-characteridentifier-charactersopt

implicit-parameter-name → $decimal-digits

Ключевые слова и пунктуация

Следующие ключевые слова зарезервированы и не могут быть использованы в качестве идентификаторов, если не помещены в одиночные кавычки, как описано выше в "Идентификаторы". Ключевые слова, кроме inout, var, и let могут быть использованы в качестве внешних имен параметров в объявлении функции или в вызове функции без необходимости помещать их в одиночные кавычки. Когда член имеет то же самое имя, что и ключевое слово, то нет необходимости экранировать это ключевое слово при помощи обратных одинарных кавычек, если только существует неоднозначность с использованием таких ключевых слов как self, Type, Protocol. Эти слова имеют особенное значение, поэтому их обязательно нужно экранировать с помощью обратных одинарных кавычек.

Ключевые слова, используемые в объявлениях: associatedtype, class, deinit, enum, extension, fileprivate, func, import, init, inout, internal, let, operator, private, protocol, public, static, struct, subscript, typealias и var.

Ключевые слова, используемые в выражениях: break, case, continue, default, defer, do, else, fallthrough, for, guard, if, in, repeat, return, switch, where, и while..

Ключевые слова, используемые в выражениях и типах: as, catch, false, is, nil, rethrows, super, self, Self, throw, throws, true, и try.

Ключевые слова, используемые в шаблонах: _.

Ключевые слова, начинающиеся со знака (#): #available, #column, #else, #elseif, #endif, #file, #function, #if, #line, #sourceLocation, #imageLiteral, #fileliteral, #colorLiteral, и #selector.

Ключевые слова, зарезервированные в определенных контекстах: associativity, convenience, dynamic, didSet, final, get, infix, indirect, lazy, left, mutating, none, nonmutating, optional, override, postfix, precedence, prefix, Protocol, required, right, set, Type, unowned, weak, и willSet. Вне контекста, в котором они используются в грамматике, они могут быть использованы в качестве идентификаторов.

Следующие токены зарезервированы как знаки препинания и не могут быть использованы в качестве пользовательских операторов: (, ), {, }, [, ], ., ,, :, ;, =, @, #, & (в качестве префиксного оператора), ->, `, ? и ! (в качестве постфиксного оператора).

Литералы

Литералы являются исходным кодом представления типа значения, например, цифры или строки.

Ниже приведены примеры литералов:

42               // целочисленный литерал

3.14159          // литерал дробного числа с плавающей точкой

"Hello, world!"  // строковый литерал

true             // логический литерал

Литерал не имеет самостоятельный тип. Вместо этого, литерал обрабатывается как имеющий бесконечную точность и выводимый тип в Swift пытается вывести тип для литерала. Например, в объявлении let x: Int8 = 42, Swift использует явную аннотацию типа (: Int8) и делает вывод, что тип целочисленного литерала 42 будет Int8. Если не доступна информация о типе, Swift делает вывод, что тип литерала является одним из дефолтных типов литерала, определенных в стандартной библиотеке Swift. Дефолтные типы (или типы по умолчанию) являются Int для целочисленных литералов, Double для литералов с плавающей точкой, String для строковых литералов и Bool для логических литералов. Например, в объявлении let str = "Hello, world", дефолтный выведенный тип литерала строки "Hello, world" будет String.

Устанавливая тип аннотации для значения литерала, тип аннотации должен быть типом, экземпляр которого будет создан из этого значения литерала. То есть, тип должен соответствовать одному из следующих стандартных протоколов библиотеки Swift: IntegerLiteralConvertible для целочисленных литералов, FloatingPointLiteralConvertible для литералов с плавающей точкой, StringLiteralConvertible для строковых литералов, и BooleanLiteralConvertible для булева литералов. Например, Int8 соответствует протоколу IntegerLiteralConvertible, и поэтому может быть использован в аннотации типа для целочисленного литерала 42 в объявлении let x: Int8 = 42.

Грамматика литерала

literal → numeric-literal | string-literal | boolean-literal | nil-literal

numeric-literal → -opt integer-literal | -optfloating-point-literal

boolean-literal → true | false

nil-literal → nil

Целочисленные литералы

Целочисленные литералы представляют собой целые значения неустановленной точности. По умолчанию целочисленные литералы выражаются в десятичной системе исчисления. Вы можете указать альтернативную базу с использованием префикса. Двоичные литералы начинаются с 0b, восьмеричные литералы начинаются с 0o, и шестнадцатеричные литералы начинаются с 0x.

Десятичные литералы содержат цифры от 0 до 9. Двоичные литералы содержат 0 и 1, восьмеричные литералы содержат от 0 до 7, и шестнадцатеричные литералы содержат от 0 до 9, а также от A до F в верхнем или нижнем регистре.

Отрицательные целочисленные литералы имеют знак минус (-) для целочисленного литерала, как в -42.

Знак подчеркивания (_) разрешен между цифрами для удобства чтения, но игнорируются, и, следовательно, не влияет на величину или значение литерала. Целочисленные литералы могут начинаться с нулей (0), но они так же игнорируются и не влияют на значение возводимое в степень или значение литерала.

Если не указано иное, дефолтный выводимый тип целочисленного литерала - это стандартный тип библиотеки Swift - Int. Стандартная библиотека Swift также определяет типы для различных размеров беззнаковых и знаковых Int.

Грамматика целочисленного литерала

integer-literal → binary-literal

integer-literal → octal-literal

integer-literal → decimal-literal

integer-literal → hexadecimal-literal

binary-literal → 0b binary-digitbinary-literal-charactersopt

binary-digit → Цифры 0 или 1

binary-literal-character → binary-digit | _

binary-literal-characters → binary-literal-character binary-literal-charactersopt

octal-literal → 0o octal-digitoctal-literal-charactersopt

octal-digit → Цифры от 0 до 7 включительно

octal-literal-character → octal-digit |  _

octal-literal-characters → octal-literal-character octal-literal-charactersopt

decimal-literal → decimal-digit decimal-literal-charactersopt

decimal-digit → Цифры от 0 до 9 включительно

decimal-digits → decimal-digitdecimal-digitsopt

decimal-literal-character → decimal-digit  | _

decimal-literal-characters → decimal-literal-character decimal-literal-charactersopt

hexadecimal-literal → 0x hexadecimal-digithexa decimal-literal-charactersopt

hexadecimal-digit → Цифры от 0 до 9 включительно, от a до f включительно или от A до F включительно

hexadecimal-literal-character → hexadecimal-digit | _

hexadecimal-literal-characters → hexadecimal-literal-character hexadecimal-literal-charactersopt

Литералы с плавающей точкой

Литералы с плавающей точкой представляют значения с плавающей точкой неуказанной точности.

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

Десятичные литералы с плавающей точкой состоят из последовательности, состоящей из десятичных цифр с последующей десятичной дробью, или десятичным экспонентом (показателем степени или порядка), или и тем и тем. Десятичная дробь состоит из десятичной точки (.), за которой идет последовательность десятичных цифр. Экспонент состоит из префикса e верхнего или нижнего регистра, за которым следует последовательность десятичных цифр, которая указывает, на 10 какой степени будет умножается это значения перед e, например, 1.25e2 будет представлено как 1.25 x 102, и принимает значение 125.0. Точно так же, .25e-2 будет представлено как 1.25 x 10-2, что равно 0.0125.

Шестнадцатиричные литералы с плавающей точкой состоят из префикса 0x, за которым может следовать шестнадцатеричная дробь, с последующим шестнадцатеричным экспонентом. Шестнадцатеричная дробь состоит из десятичной точки, за которой следует последовательность шестнадцатеричного разряда. Экспонент состоит из префикса p верхнего или нижнего регистра, за которым следует последовательность десятичных цифр, которая указывает на степень, в которую будет возводиться 2, стоящая перед p. Например, 0xFp2 это 15 x 22, что равно 60. Точно так же, 0xFp-2 составляет 15 x 2-2, и имеет значение 3.75.

Отрицательные литералы с плавающей точкой имеют знак минус (-) перед литералом с плавающей точкой, например -42.5.

Знак подчеркивания (_) разрешен между цифрами для удобства чтения, но игнорируются, и, следовательно, не влияет на значение литерала. Литералы с плавающей точкой могут начинаться с нуля (0), но он также же игнорируются и не влияют на значение возводимого в степень основания или на значение литерала.

Если не указано иное, по умолчанию выводимый тип с плавающей точкой - это стандартный тип библиотеки Swift Double, который представляет собой 64-битное число с плавающей точкой. Стандартная библиотека Swift также определяет тип Float, представляющее собой 32-битное число с плавающей точкой.

Грамматика числа с плавающей точкой

floating-point-literal → decimal-literal decimal-fractionopt decimal-exponentopt

floating-point-literal → hexadecimal-literal hexadecimal-fractionopt hexadecimal-exponent

decimal-fraction → .decimal-literal

decimal-exponent → floating-point-esignopt decimal-literal

hexadecimal-fraction → .hexadecimal-digit hexadecimal-literal-charactersopt

hexadecimal-exponent → floating-point-psignopt decimal-literal

floating-point-e → e | E

floating-point-p → p | P

sign → + | -

Строковые литералы

Строковый литерал представляет собой последовательность символов, взятых в двойные кавычки, и выглядит вот так:

"characters"

Строковые литералы не могут содержать неэкранированные двойные кавычки ("), неэкранированный обратный слеш (\), возврат каретки или перевод строки.

Многострочный строковый литерал заключен тремя двойными кавычками и выглядит следующим образом:

"""

characters

"""

В отличие от однострочного строкового литерала, многострочный строковый литерал может содержать неэксклюзивные двойные кавычки («), возврат каретки и перевод строки. Многострочный не может содержать три неизолированные двойные кавычки рядом друг с другом.

Разрыв строки после """, который начинается в многострочном строковом литерале, не является частью строки. Разрыв строки перед """, который завершает литерал, также не является частью строки. Чтобы создать многострочный строковый литерал, который начинается или заканчивается переводом строки, введите пустую строку в качестве ее первой или последней строки.

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

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

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

Специальные символы могут быть включены в строковые литералы, используя следующие экранированные последовательные записи:

Нулевой символ (\0)

Обратный слеш (\\)

Горизонтальная табуляция (\t)

Перевод строки (\n)

Возврат каретки (\r)

Двойные кавычки (\")

Одиночные кавычки (\')

Скаляр Юникода (\u {n}), где n между первым и восьмым шестнадцатеричными символами

Значение выражения может быть вставлено в строку литерала, поместив выражение в скобки после обратного слеша (\). Интерполированное выражение может содержать строковый литерал, но не может содержать неэкранированный обратный слеш (\), возврат каретки или перевод строки.

Например, все следующие строковые литералы имеют одинаковое значение:

"1 2 3"

"1 2 \("3")"

"1 2 \(3)"

"1 2 \(1 + 2)"

let x = 3; "1 2 \(x)"

Дефолтный выводимый тип строчного литерала является String. Для получения дополнительной информации о типе String, см. Строки и символы и Справка по структуре String.

Строковые литералы, которые объединяются с оператором +, делают это во время компиляции. Например, значения textA и textB в примере ниже, являются идентичными, и объединения во время компиляции не происходит.

let textA = "Hello " + "world"

let textB = "Hello world"

Грамматика строкового литерала

string-literal → static-string-literal | interpolated-string-literal

static-string-literal → " quoted-text opt "

static-string-literal → """ multiline-quoted-text opt """

quoted-text → quoted-text-item quoted-text opt

quoted-text-item → escaped-character

quoted-text-item → Any Unicode scalar value except ", \, U+000A, or U+000D

multiline-quoted-text → multiline-quoted-text-item multiline-quoted-text opt

multiline-quoted-text-item → escaped-character

multiline-quoted-text-item → Any Unicode scalar value except \

multiline-quoted-text-item → escaped-newline

interpolated-string-literal → " interpolated-text opt "

interpolated-string-literal → """ multiline-interpolated-text opt """

interpolated-text → interpolated-text-item interpolated-text opt

interpolated-text-item → \( expression ) | quoted-text-item

multiline-interpolated-text → multiline-interpolated-text-item multiline-interpolated-text opt

multiline-interpolated-text-item → \( expression ) | multiline-quoted-text-item

escaped-character → \0 | \\ | \t | \n | \r | \" | \'

escaped-character → \u { unicode-scalar-digits }

unicode-scalar-digits → Between one and eight hexadecimal digits

escaped-newline → \ whitespace opt line-break

Операторы

Стандартная библиотека Swift определяет ряд операторов для вашего использования, многие из которых обсуждаются в Базовые операторы и Продвинутые операторы. В настоящем разделе описывается, какие символы могут быть использованы для определения пользовательских операторов.

Пользовательские операторы могут начинаться с одного из символов ASCII: /, =, -, +, !, *, %, <, >, &, |, ^, ?, или ~, или одного из символов Unicode, которые мы разберем позднее (которые, среди других, включают в себя символы из Математических Операторов, Смешанные Символы и Графические метки блоков Unicode. После первого символа, использование других символов Unicode также является допустимым.

Можно также определить пользовательские операторы, которые начинаются с точки (.). Эти операторы могут содержать дополнительные точки, такие как .+.. Если оператор не начинается с точки, он не может содержать точку в другом месте. Например, +.+ Рассматривается как оператор +, за которым следует оператор +..

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

Заметка

Токены =, ->, //, /*, */, ., префиксные операторы <, &, и ?, инфиксный оператор ?, и постфиксные операторы >, !, и ? зарезервированы. Эти токены не могут быть перезагружены, и не могут быть использованы в качестве пользовательских операторов.

Пробелы вокруг оператора используется для того, чтобы понять, используется ли оператор в качестве префиксного оператора, постфиксного оператора или бинарного оператора. Такое поведение представлено в следующих правилах:

Если у оператора пробелы по обеим сторонам или их нет вообще, то он рассматривается как бинарный оператор. Например, оператор +++ в a+++b и a +++ b рассматривается как бинарный оператор.

Если у оператора пробелы только слева, он рассматривается как префиксный унарный оператор. В качестве примера, оператор +++  в a +++b  рассматривается как префиксный унарный оператор.

Если у оператора пробелы только на правой стороне, он рассматривается как постфиксный унарный оператор. Например, оператор +++ в a+++ b рассматривается как унарный постфиксный оператор.

Если у оператора нет вообще пробелов слева, но за ним сразу идет точка (.), то это унарный постфиксный оператор. Например, оператор +++ в a+++.b рассматривается как унарный постфиксный оператор (a+++ .b, а не a +++ .b).

В целях следования настоящим правилам, символы (, [, и { перед оператором, символами ), ], и }, после оператора, и символов ,, ;, и : также рассматриваются в качестве пробелов.

Для вышеуказанных правил существует одно “но”. Если у предопределенного оператора ! или ? нет пробелов слева, то он рассматривается как постфиксный оператор, независимо от того, имеет ли он пробел справа. Чтобы использовать ? в качестве оператора опциональной цепочки, то у него не должно быть пробела слева. Для того, чтобы использовать его в тернарном операторе (? :), у него должны быть пробелы с двух сторон.

В некоторых конструкциях, операторы, начинающиеся с < или > могут быть разделены на два или более токена. То, что осталось обрабатывается так же, и может быть разделено снова. В результате, нет необходимости использовать пробел для устранения неоднозначности между символами замыкания > в конструкциях, таких как Dictionary<String, Array<Int>>. В этом примере, символы замыкания > не рассматриваются как один токен, который затем может быть неправильно истолкован в качестве a оператора побитового смещения >>.

Для того чтобы узнать, как определять новые, пользовательские операторы, см. Пользовательские операторы. Для того чтобы узнать, как перегружать существующие операторы, см. Операторные функции.

Грамматика операторов

operator → operator-head operator-charactersopt

operator → dot-operator-head dot-operator-characters

operator-head → / | = | - | + | ! | * | % | < | > | & | | | ^ | ~ | ?

operator-head → U+00A1–U+00A7

operator-head → U+00A9 or U+00AB

operator-head → U+00AC or U+00AE

operator-head → U+00B0–U+00B1, U+00B6, U+00BB, U+00BF, U+00D7, or U+00F7

operator-head → U+2016–U+2017 or U+2020–U+2027

operator-head → U+2030–U+203E

operator-head → U+2041–U+2053

operator-head → U+2055–U+205E

operator-head → U+2190–U+23FF

operator-head → U+2500–U+2775

operator-head → U+2794–U+2BFF

operator-head → U+2E00–U+2E7F

operator-head → U+3001–U+3003

operator-head → U+3008–U+3030

operator-character → operator-head

operator-character → U+0300–U+036F

operator-character → U+1DC0–U+1DFF

operator-character → U+20D0–U+20FF

operator-character → U+FE00–U+FE0F

operator-character → U+FE20–U+FE2F

operator-character → U+E0100–U+E01EF

operator-characters → operator-character operator-charactersopt

dot-operator-head → .

dot-operator-character → . | operator-character

dot-operator-characters → dot-operator-characterdot-operator-charactersopt

binary-operator → operator

prefix-operator → operator

postfix-operator → operator

Типы

В Swift есть два вида типов: именованные типы и составные типы. Именованный тип представляет собой тип, которому может быть дано определенное имя, когда он определен. Именованные типы включают в себя классы, структуры, перечисления и протоколы. Например, у экземпляра определенного пользователем класса MyClass есть тип MyClass. В дополнение к определенным пользователем именованным типам, стандартная библиотека Swift определяет множество часто используемых именованных типов, в том числе те, которые представляют собой массивы, словари и опциональные значения.

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

Составной тип представляет собой тип без имени, и определяется самостоятельно в языке Swift. Есть два составных типа: типы функций и типы кортежей. Составной тип может содержать именованные типы и другие составные типы. Например, тип кортежа (Int, (Int, Int)) содержит два элемента: первый именованный тип Int, а второй составной тип (Int, Int).

В этой главе рассматриваются типы, определяемые самостоятельно в языке Swift и описывается поведение выводимых типов в Swift.

Грамматика типа

type → array-type

type → dictionary-type

type → function-type

type → type-identifier

type → tuple-type

type → optional-type

type → implicitly-unwrapped-optional-type

type → protocol-composition-type

type → metatype-type

type → Any

type → Self

type → ( type )

Аннотация типа

Аннотация типа явно указывает на тип переменной или выражения. Аннотации типов начинаются с двоеточия (:) и заканчиваются именем типом, как показано в следующем примере:

let someTuple: (Double, Double) = (3.14159, 2.71828)

func someFunction(a: Int) { /* ... */ }

В первом примере выражению someTuple предписано иметь тип кортежа (Double, Double). Во втором примере, параметру a функции someFunction предписано иметь тип Int.

Аннотации типа могут содержать дополнительный список атрибутов типа перед типом.

Грамматика аннотации типа

type-annotation → :attributesopt  inoutopt type

Идентификатор типа

Идентификатор типа относится либо к именованному типу, либо к псевдониму именованного или составного типа.

По большей части, идентификатор типа непосредственно ссылается на именованный тип с тем же именем, что и идентификатор. Например, Int является идентификатором типа, который непосредственно ссылается на именованный тип Int, и идентификатор типа Dictionary<String, Int> непосредственно ссылается на именованный тип Dictionary<String, Int>.

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

typealias Point = (Int, Int)

let origin: Point = (0, 0)

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

var someValue: ExampleModule.MyType

Грамматика идентификатора типа

type-identifier → type-name  generic-argument-clauseopt  |  type-name  generic-argument-clauseopt  . type-identifier type-name → identifier

Тип кортежа

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

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

Если элемент кортежа имеет имя, то имя является частью типа:

var someTuple = (top: 10, bottom: 12) // someTuple имеет тип (top: Int, bottom: Int)

someTuple = (top: 4, bottom: 42) // OK: имена совпадают

someTuple = (9, 99) // OK: имена выводятся

someTuple = (left: 5, right: 5) // Error: имена не совпадают

Все типы кортежей содержат два или более типов, за исключением Void, который является псевдонимом типа для пустого типа кортежа, ().

Грамматика кортежного типа

tuple-type → (tuple-type-bodyopt) tuple-type-element-list → tuple-type-element  |  tuple-type-element,tuple-type-element-list tuple-type-element →  element-name   type-annotation  |  type element-name → identifier

Функциональный тип

Тип функции представляет собой тип функции, метода или замыкания и состоит из параметра и возвращаемого типа, разделенного стрелкой (->):

тип параметра -> возвращаемый тип

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

Параметр типа функции () -> T (где T произвольный тип) может применять атрибут autoclosure для неявного создания замыкания для своих вызовов. Это обеспечивает синтаксически удобный способ отложить вычисление выражения, без необходимости написания явного замыкания при вызове функции. Подробнее об autoclosure в разделе Автозамыканиях.

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

Для того, чтобы указать параметр In-Out, приставьте тип параметра с ключевым словом inout. Вы не можете обозначать вариативный параметр или возвращаемый тип ключевым словом inout. In-Out параметры подробнее рассматриваются в разделе Сквозные параметры.

Имена аргументов не являются частью типа соответствующей функции:

func someFunction(left: Int, right: Int) {}

func anotherFunction(left: Int, right: Int) {}

func functionWithDifferentLabels(top: Int, bottom: Int) {}

var f = someFunction // The type of f is (Int, Int) -> Void, not (left: Int, right: Int) -> Void.

f = anotherFunction // OK

f = functionWithDifferentLabels // OK

func functionWithDifferentArgumentTypes(left: Int, right: String) {}

f = functionWithDifferentArgumentTypes // Ошибка

func functionWithDifferentNumberOfArguments(left: Int, right: Int, top: Int) {}

f = functionWithDifferentNumberOfArguments // Ошибка

Поскольку аргументы Labels не являются частью типа функции, вы опускаете их при написании типа функции.

var operation: (lhs: Int, rhs: Int) -> Int // Error

var operation: (_ lhs: Int, _ rhs: Int) -> Int // OK

var operation: (Int, Int) -> Int // OK

Если тип функции включает в себя более чем одну стрелку (->), типы функции группируются справа налево. Например, тип функции Int -> Int -> Int понимается как Int -> (Int -> Int), то есть функция, которая принимает Int и возвращает другую функцию, которая принимает и возвращает Int.

Типы функций, которые могут генерировать ошибку должны быть отмечены ключевым словом throws, а также типы функций, которые могут повторно генерировать ошибку должны быть отмечены ключевым словом rethrows. Ключевое слово throws является частью функционального типа, а негенерирующие функции являются подклассами генерирующих функций. В результате, вы можете использовать негенерирующую функцию там же, где и генерирующую. Генерирующие и повторно генерирующие функции описаны в "Генерирующие функции и методы" и "Повторно генерирующие методы и функции".

Ограничения для не ускользающей замыканий

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

Параметр, который является не ускользающей функцией, не может быть передан в качестве аргумента другому параметру несобранной функции. Это ограничение помогает Swift выполнять больше проверок на конфликтный доступ к памяти во время компиляции, а не во время выполнения. Например:

let external: (() -> Void) -> Void = { _ in () }

func takesTwoFunctions(first: (() -> Void) -> Void, second: (() -> Void) -> Void) {

first { first {} } // Ошибка

second { second {} } // Ошибка

first { second {} } // Ошибка

second { first {} } // Ошибка

first { external {} } // OK

external { first {} } // OK

}

В приведенном выше коде оба параметра, которые принимают значения TwoFunctions (first: second , являются функциями. Ни один из параметров не отмечен как @escaping, поэтому они оба не отображаются в результате.

Четыре вызова функций, отмеченные «Ошибка» в приведенном выше примере, вызывают ошибки компилятора. Поскольку первый и второй параметры являются не ускользающими функциями, они не могут передаваться в качестве аргументов другому параметру не ускользающей функции. Напротив, два вызова функций, отмеченные «ОК», не вызывают ошибки компилятора. Эти вызовы функций не нарушают ограничение, потому что внешний не является одним из параметров takeTwoFunctions (first: second .

Если вам нужно избегать этого ограничения, отметьте один из параметров как экранирование или временно конвертируйте один из параметров функции в функцию экранирования с помощью функции withoutActuallyEscaping (_: do:). Информацию об избежании конфликта доступа к памяти см. В разделе Безопасность хранения.

Грамматика функционального типа

function-type → attributesopt  function-type-argument-clause  throwsopt  ->  type function-type → attributesopt  function-type-argument-clause  rethrows  ->  type function-type-argument-clause → () function-type-argument-clause → (function-type-argument-list...opt) function-type-argument-list → function-type-argument | function-type-argument , function-type-argument-list function-type-argument → attributesopt  inoutopt  type | argument-label  type-annotation argument-label → identifier

Типы массива

Язык Swift предоставляет следующий синтаксический сахар для стандартной библиотеки Swift тип Array<Element>:

[type]

Другими словами, следующие два объявления эквивалентны:

let someArray: Array<String> = ["Alex", "Brian", "Dave"]

let someArray: [String] = ["Alex", "Brian", "Dave"]

В обоих случаях константа someArray объявляется как массив строк. Элементы массива могут быть доступны через индексацию, указав допустимое значение индекса в квадратных скобках: someArray[0] относится к элементу с индексом 0, "Alex".

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

var array3D: [[[Int]]] = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]

При обращении к элементам в многомерном массиве, самый левый индекс сабскриптов относится к элементу с этим индексом в самом нижнем массиве. Следующий индекс сабскрипта с правой стороны относится к элементу с этим индексом в массиве, вложенном на предыдущем уровне. И так далее. Это означает, что в приведенном выше примере, array3D[0] относится к [[1, 2], [3, 4]], array3D[0][1] относится к [3, 4], и array3D[0][1][1] относится к значению 4.

Более подробно о стандартном типе Array библиотеки Swift, см. "Массивы".

Грамматика типа массива

array-type → [type]

Тип словаря

Язык Swift язык предоставляет следующий синтаксический сахар для типа Dictionary<Key, Value> стандартной библиотеки Swift:

[key type: value type]

Другими словами, следующие два объявления эквивалентны:

let someDictionary: [String: Int] = ["Alex": 31, "Paul": 39]

let someDictionary: Dictionary<String, Int> = ["Alex": 31, "Paul": 39]

В обоих случаях константа someDictionary объявляется как словарь со строками в качестве ключей и целых чисел в качестве значений.

Значения словаря могут быть доступны через индексацию при указании соответствующего ключа в квадратных скобках: someDictionary["Alex"] относится к значению, связанному с ключом "Alex". Сабскрипт возвращает опциональное значение типа значения словаря. Если указанный ключ не содержится в словаре, сабскрипт возвращает nil.

Основной тип ключа словаря должен соответствовать протоколу Hashable стандартной библиотеки Swift.

Подробнее см. Словари.

Грамматика типа словаря

dictionary-type → [type : type]

Опциональный тип

Язык Swift определяет постфикс ? как синтаксический сахар для именованного типа  Optional<Wrapped>, определенного в стандартной библиотеке Swift. Другими словами, следующие два объявления эквивалентны:

var optionalInteger: Int?

var optionalInteger: Optional<Int>

В обоих случаях переменная optionalInteger объявляется так, чтобы иметь тип опционального Int. Обратите внимание, что никаких пробелов не должно появляться между именем типа и ?.

Тип Optional<Wrapped> является перечислением с двумя кейсами, None и Some(Wrapped), которые используются для представления значений, которые могут присутствовать, а могут и не присутствовать. Любой тип может быть явно объявлен (или неявно преобразован) так, чтобы быть опциональным типом. Если вы не укажете первоначальное значение при объявлении опциональной переменной или свойства, его значение автоматически устанавливается на nil.

Если экземпляр опционального типа содержит значение, вы можете получить доступ к этому значению, используя постфиксный оператор !, как показано ниже:

optionalInteger = 42

optionalInteger! // 42

Использование оператора ! для извлечения опционала, имеющего значение nil, приводит к ошибке во время выполнения.

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

Для получения дополнительной информации см. Опционалы.

Грамматика опционального типа

optional-type → type  ?

Неявно извлеченный опциональный тип

Язык Swift определяет постфикс ! как синтаксический сахар для именованного типа ImplicitlyUnwrappedOptional<Wrapped>, определенного в стандартной библиотеке Swift. Другими словами, следующие два объявления эквивалентны:

var implicitlyUnwrappedString: String!

var explicitlyUnwrappedString: Optional<String>

Обратите внимание, что не должно появиться никаких пробелов между именем типа и !.

Так как неявное извлечение изменяет смысл объявления, которое содержит тип, опциональные типы, которые являются вложенными внутри кортежа или универсального типа, например элемент словаря или массива, не могут быть обозначены как неявно извлеченные. Например:

let tupleOfImplicitlyUnwrappedElements: (Int!, Int!) // Ошибка

let implicitlyUnwrappedTuple: (Int, Int)! // OK

let arrayOfImplicitlyUnwrappedElements: [Int!] // Ошибка

let implicitlyUnwrappedArray: [Int]! // OK

Так как неявно извлеченные опционалы имеют тот же тип Optional<Wrapped>, то можете использовать неявно извлеченные опционалы в тех же местах в вашем коде, в которых можно использовать опционалы. Например, вы можете присвоить значения неявно извлеченных опционалов переменным, константам и свойствам опционалов и наоборот.

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

Поскольку значение неявно извлеченного опционала автоматически распаковывается, когда вы его используете, то нет никакой необходимости использовать оператор ! для его разворачивания. Тем не менее, если вы пытаетесь использовать неявно распакованный опционал, имеющий значение nil, у вас будет ошибка выполнения.

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

Для получения дополнительной информации см. в разделе Неявно извлеченные опционалы.

Грамматика неявно извлеченного опционального типа

implicitly-unwrapped-optional-type → type !

Тип композиции протоколов

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

Тип композиции протоколов выглядит следующим образом:

Protocol 1 & Protocol 2

Тип композиции протоколов позволяет указать значение, тип которого соответствует требованиям нескольких протоколов без необходимости явно определять новый, именованный протокол, который наследует от каждого протокола, которому вы хотите, чтобы тип соответствовал. Например, указать тип композиции протоколов ProtocolA & ProtocolB & ProtocolC так же эффективно, как определить новый протокол ProtocolD, который наследует от ProtocolA, ProtocolB, и ProtocolC, но без необходимости введения нового имени.

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

Грамматика типа композиции протоколов

protocol-composition-type → protocol-identifier & protocol-composition-continuation protocol-composition-continuation → protocol-identifier  | protocol-composition-type protocol-identifier → type-identifier

Тип метатипа

Тип метатипа относится к типу любого типа, в том числе к типам класса, типам конструкций, типам перечислений и типам протоколов.

Метатип типа класса, структуры или перечисления является именем этого типа, за которым идет .Type. Метатип типа протокола - не конкретный тип, который соответствует протоколу во время выполнения - это имя этого протокола с последующим .Protocol. Например, метатип типа класса SomeClass является SomeClass.Type и метатип протокола SomeProtocol является SomeProtocol.Protocol.

Вы можете использовать выражение постфикса self для доступа к типу в качестве значения. Например, SomeClass.self возвращает сам SomeClass, а не экземпляр SomeClass. И SomeProtocol.self возвращает сам SomeProtocol, а не экземпляр типа, который соответствует SomeProtocol во время выполнения задачи. Вы можете использовать выражение dynamicType с экземпляром типа для доступа к экземпляру динамической идентификации типа в период выполнения, в качестве значения, как показано в следующем примере:

class SomeBaseClass {

class func printClassName() {

print("SomeBaseClass")

}

}

class SomeSubClass: SomeBaseClass {

override class func printClassName() {

print("SomeSubClass")

}

}

let someInstance: SomeBaseClass = SomeSubClass()

// someInstance имеет тип во время компиляции SomeBaseClass,

// во время исполнения типом someInstance является SomeSubClass

type(of: someInstance).printClassName()

// Выведет "SomeSubClass"

Для получения дополнительной информации, см. type (of: ) стандартной библиотеке Swift.

Используйте выражение инициализатора для того, чтобы создать экземпляр типа из значения типа этого метатипа. Для экземпляров класса инициализатор, который вызывается, должен быть отмечен ключевым словом required, или весь класс должен быть отмечен ключевым словом final.

class AnotherSubClass: SomeBaseClass {

let string: String

required init(string: String) {

self.string = string

}

override class func printClassName() {

print("AnotherSubClass")

}

}

let metatype: AnotherSubClass.Type = AnotherSubClass.self

let anotherInstance = metatype.init(string: "some string")

Грамматика метатипа

metatype-type → type . Type  |  type.  Protocol

Наследование типа

Наследование типа используется для указания того, у какого класса наследует именованный тип и каким протоколам он соответствует. Наследование типа также используется для указания классовых требований по протоколу. Наследование типа начинается с двоеточия (:), за которым следуют либо требования class, либо список идентификаторов типа, либо и то и то.

Типы классов могут наследовать от одного суперкласса и соответствовать любому количеству протоколов. При определении класса, имя суперкласса должно появиться в начале списка идентификаторов типа, за которым уже будут следовать протоколы, которым класс должен соответствовать. Если класс не наследует от другого класса, список может начинаться вместо этого с протокола. Подробнее см. Наследование.

Другие именованные типы могут только наследовать или соответствовать списку протоколов. Типы протоколов могут наследовать от любого количества других протоколов. Когда тип протокола наследует от других протоколов, и требования от всех протоколов собираются в совокупность, и любой тип, наследующий от текущего протокола, должен соответствовать всем этим требованиям. Как обсуждалось ранее, вы можете включить ключевое слово class в качестве первого пункта в условие типа наследования, чтобы отметить объявление протокола с class-требованием.

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

Грамматика наследования типа

type-inheritance-clause → : class-requirement,type-inheritance-list type-inheritance-clause → : class-requirement type-inheritance-clause → : type-inheritance-list type-inheritance-list → type-identifier  |  type-identifier , type-inheritance-list class-requirement → class

Вывод типа

Swift широко использует вывод типа, что позволяет опустить тип или часть типа множества переменных и выражений в коде. Например, вместо написания var x: Int = 0, вы можете написать var x = 0, опуская тип полностью, компилятор правильно понимает, что x это значение типа Int. Кроме того, вы можете опустить часть типа, когда полный тип может быть выведен из контекста. Например, когда вы пишете let dict: Dictionary = ["A": 1], компилятор делает вывод, что dict имеет тип Dictionary<String, Int>.

В обоих приведенных выше примерах информация о типе передается от листьев дерева выражения к его корню. То есть, для типа x в var x: Int = 0 сначала проводится проверка типа 0, а затем эта информация передается корню (переменной x).

В Swift, информация о типе может также протекать в обратном направлении: от корня к листьям. В следующем примере, явный тип аннотации (: Float) на константе eFloat приводит к тому, что у числовой константы 2,71828 выведенный тип Float вместо Double.

let e = 2.71828 // Выведенным типом e является Double.

let eFloat: Float = 2.71828 // Выведенным типом eFloat является Float.

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

Выражения

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

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

Грамматика выражений

expression → try-operator opt prefix-expression binary-expressionsopt

expression-list → expression  | expression,expression-list

Префиксные выражения

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

Подробнее об этих операторах см. Базовые операторы и Продвинутые операторы.

Получить информацию об операторах, предоставляемых стандартной библиотекой Swift, см. Operator Declarations.

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

Грамматика префиксных выражений

prefix-expression → prefix-operatoropt postfix-expression

prefix-expression → in-out-expression

in-out-expression → & identifier

Оператор Try

Выражение Try состоит из оператора try, за которым следует выражение, которое может генерировать ошибку. Выглядит вот так:

try выражение

Опциальное выражение try состоит из оператора try?, за которым следует выражение, которое может генерировать ошибку. Выглядит вот так:

try? выражение

Если выражение не генерирует ошибку, то значение опционального выражения try является опционалом, содержащий значение выражения. Другими словами значение опционального выражения try будет равно nil.

Выражение forced-try состоит из оператора try!, за которым следует выражение, которое может генерировать ошибку. Выглядит вот так:

try! выражение

Если выражение генерирует ошибку, выходит ошибка выполнения.

Когда выражение слева бинарного оператора отмечено try, try?, или try!, оператор применяется ко всему бинарному выражению. Тем не менее, вы можете использовать круглые скобки, чтобы явно указать сферу применения оператора.

sum = try someThrowingFunction() + anotherThrowingFunction()   // try применяется к обоим вызовам функций

sum = try (someThrowingFunction() + anotherThrowingFunction()) // try применяется к обоим вызовам функций

sum = (try someThrowingFunction()) + anotherThrowingFunction() // Error: try применяется только к первой функции

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

Подробнее о try, try?, и try! см. Обработка ошибок.

Грамматика выражения try

try-operator → try | try? | try!

Бинарные выражения

Бинарные выражения объединяют инфиксный бинарный оператор и выражение, которое принимает левосторонние и правосторонние аргументы. Вот так оно выглядит:

аргумент слева оператор аргумент справа

Подробнее см. Базовые операторы и Продвинутые операторы.

Подробнее о операторах, представленных в стандартной библиотеке Swift читайте в Operator Declarations.

Заметка

Во время парсинга, выражение из бинарных операторов выглядит как простой список. Этот список превращается в дерево через порядок выполнения операций. Например, выражение 2 + 3 * 5 изначально понимается как список из пяти пунктов, 2, +, 3, *, и 5. Этот процесс преобразует его в дерево (2 + (3 * 5)).

Грамматика бинарных выражений

binary-expression → binary-operator prefix-expression

binary-expression → assignment-operator try-operatoropt prefix-expression

binary-expression → conditional-operator try-operatoropt prefix-expression

binary-expression → type-casting-operator

binary-expressions → binary-expression binary-expressionsopt

Оператор присваивания

Оператор присваивания устанавливает новое значение для данного выражения. Выглядит он вот так:

выражение = значение

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

(a, _, (b, c)) = ("test", 9.45, (12, 3))

// a равен "test", b равно 12, c равно 3, а 9.45 игнорируется

Оператор присваивания не возвращает никакого значения.

Грамматика оператора присваивания

assignment-operator → =

Тернарный условный оператор

Тернарный условный оператор принимает значение одного из двух заданных значений в зависимости от условия. Выглядит это так:

условие ? выражение при true : выражение при false

Если условие true, условный оператор вычисляет первое выражение и возвращает его значение. В противном случае, он вычисляет второе выражение и возвращает его значение. Неиспользованное выражение не вычисляется.

Подробнее см. Тернарный условный оператор.

Грамматика условного оператора

conditional-operator → ?expression:

Операторы приведения типа

Существует четыре типа операторов приведения типа: оператор is, оператор as, оператор as?, и оператор as!.

Выглядят они вот так:

выражение is тип

выражение as тип

выражение as? тип

выражение as! тип

Оператор is во время выполнения задачи проверяет может ли выражение быть приведено к указанному типу. Возврат true происходит, если выражение может быть приведено к указанному типу, в противном случае, будет возвращено false.

Оператор as выполняет приведение, когда во время компилляции становится известно, что приведение пройдет успешно, например приведение к базовому типу или bridging. Приведение к базовому типу позволяет использовать выражение как экземпляр супертипа своего типа, без использования промежуточной переменной. Следующие подходы эквивалентны:

func f(_ any: Any) { print("Функция для Any") }

func f(_ int: Int) { print("Функция для Int") }

let x = 10

f(x)

// Выведет "Функция для Int"

let y: Any = x

f(y)

// Выведет "Функция для Any"

f(x as Any)

// Выведет "Функция для Any”

Bridging позволяет использовать выражение типа стандартной библиотеки Swift, такое как String, в качестве соответствующего типа Foundation, такое как NSString, без необходимости создания нового экземпляра. Для получения дополнительной информации см. Типы фреймворка Foundation.

Оператор as? выполняет условное приведение выражения к указанному типу. Оператор as? возвращает опционал указанного типа. Во время выполнения, если приведение завершается успешно, то значение выражения заворачивают в опционал и возвращают, в противном случае, возвращаемое значение равно nil. Если приведение к указанному типу гарантированно провалится или гарантированно пройдет успешно, то возникнет ошибка во время компилляции.

Оператор as! выполняет принудительное приведение выражения к указанному типу. Оператор as! возвращает значение указанного типа, а не опционального. Если приведение терпит неудачу, возникает ошибка выполнения. Поведение x as! T такое же, как поведение (x as? T)!.

Подробнее см. Приведение типов.

Грамматика операторов приведения типа

type-casting-operator → is type type-casting-operator → as type type-casting-operator → as ? type type-casting-operator → as ! type

Первичные выражения

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

Грамматика первичных выражений

primary-expression → identifier generic-argument-clause opt

primary-expression → literal-expression

primary-expression → self-expression

primary-expression → superclass-expression

primary-expression → closure-expression

primary-expression → parenthesized-expression

primary-expression → tuple-expression

primary-expression → implicit-member-expression

primary-expression → wildcard-expression

primary-expression → key-path-expression

primary-expression → selector-expression

primary-expression → key-path-string-expression

Выражения литерала

Литеральное выражение состоит либо из обычного литерала (например, строки или числа), или литерала массива или словаря, или одного из следующих специальных литералов:

Литерал

Тип

Значение

#file

String

Имя файла, в котором он появляется.

#line

Int

Номер строки, в которой он появляется.

#column

Int

Номер столбца, с которого он начинается.

#function

String

Имя объявления, в котором он появляется.

Внутри функции значение #function - это имя этой функции, внутри метода - это имя этого метода, внутри свойства геттера или сеттера - это имя этого свойства, внутри специальных элементов, таких как init или subscript - это имя ключевого слова, а на верхнем уровне файла - это имя текущего модуля.

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

func logFunctionName(string: String = #function) {

print(string)

}

func myFunction() {

logFunctionName() // Выведет "myFunction()".

}

Литерал массива представляет собой упорядоченную совокупность значений. Он выглядит следующим образом:

[значение 1, значение 2, ...]

Последнее выражение в массиве может сопровождаться дополнительной запятой. Значение литерала массива имеет тип [T], где T - это тип выражения внутри него. Если есть выражения нескольких типов, то T их ближайший общий родительский тип. Пустые литералы массива записываются с использованием пустой пары квадратных скобок и могут быть использованы для создания пустого массива заданного типа.

var emptyArray: [Double] = []

Литерал словаря представляет собой неупорядоченный набор пар ключ-значение. Выглядит он так:

[ключ 1: значение 1, ключ 2: значение 2, ...]

Последнее выражение в словаре может сопровождаться дополнительной запятой. Значение литерала словаря имеет тип [Key: Value] ([Ключ: Значение]), где Key - это тип его ключевых выражений и Value - это тип его выражений значений. Если есть выражения нескольких типов, то Key и Value являются ближайшим супертипом для их соответствующих значений. Пустой литерал словаря пишется как двоеточие внутри пары скобок ([:]), чтобы можно было отличить его от пустого литерала массива. Вы можете использовать пустой литерал словаря для создания пустого литерала словаря установленных ключей и значений типов.

var emptyDictionary: [String: Double] = [:]

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

Информацию об использовании литералов песочниц в Xcode см. в разделе Добавление цвета, файла или литерала изображения в справке Xcode.

Грамматика выражения литерала

literal-expression → literal

literal-expression → array-literal  | dictionary-literal |  playground-literal

literal-expression → #file  | #line | #column  |  #function array-literal → [array-literal-itemsopt] array-literal-items → array-literal-item,opt  array-literal-item,array-literal-items array-literal-item → expression dictionary-literal → [dictionary-literal-items] |[:] dictionary-literal-items → dictionary-literal-item,opt dictionary-literal-item,dictionary-literal-items dictionary-literal-item → expression:expression playground-literal → #colorLiteral ( red : expression, green : expression, blue : expression, alpha : expression) playground-literal → #fileLiteral ( resourceName : expression) playground-literal → #imageLiteral ( resourceName : expression)

Выражение Self

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

self

self.member name

self[subscript index]

self(initialiser arguments)

self.init(initialiser arguments)

В инициализаторе, сабскрипте, или методе экземпляра, self относится к текущему экземпляру типа, в котором находится. В типе метода self относится к текущему типу, в котором находится.

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

class SomeClass {

var greeting: String

init(greeting: String) {

self.greeting = greeting

}

}

В mutating методе типа значения, вы можете назначить новый экземпляр этого типа значения на self. Например:

struct Point {

var x = 0.0, y = 0.0

mutating func moveBy(x deltaX: Double, y deltaY: Double) {

self = Point(x: x + deltaX, y: y + deltaY)

}

}

Грамматика выражения self

self-expression → self  self-method-expression  | self-subscript-expression | self-initializer-expression self-method-expression → self.identifier self-subscript-expression → self[expression-list] self-initializer-expression → self.init

Выражение суперкласса

Выражение суперкласса позволяет классу взаимодействовать с его суперклассом. И имеет одну из следующих форм:

super.имя члена

super[индекс сабскрипта]

super.init(аргументы инициализатора)

Первая форма используется для доступа к элементу суперкласса. Вторая форма используется для доступа к реализации суперкласса по индексу сабскрипта. Третья форма используется для доступа к инициализатору суперкласса.

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

Грамматика выражения суперкласса

superclass-expression → superclass-method-expression  | superclass-subscript-expression | superclass-initializer-expression

superclass-method-expression → super.identifier superclass-subscript-expression → super[expression-list] superclass-initializer-expression → super.init

Выражение замыкания

Выражение замыкания создает замыкание. В других языках программирования оно известно как лямбда или анонимная функция. Также как при объявлении функции, замыкание содержит заявления, которые выполняет, и вылавливает константы и переменные из области видимости. Имеет следующий вид:

{ (parameters) -> return type in

statements

}

Параметры имеют ту же форму, что и параметры в объявлении функции, как описано в Функции.

Есть несколько специальных форм, которые позволяют замыканиям быть записанными более кратко:

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

Замыкание может опускать имена своих параметров. Затем эти параметры неявно называются $, и за ними следуют: $0, $1, $2, и так далее.

Замыкание, которое состоит только из одного выражения, возвращает значение этого выражения. Содержание этого выражения также учитывается при выполнении вывода типа на окружающее выражение.

Следующие выражения замыкания эквивалентны:

myFunction {

(x: Int, y: Int) -> Int in

return x + y

}

myFunction {

(x, y) in

return x + y

}

myFunction { return $0 + $1 }

myFunction { $0 + $1 }

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

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

Для получения дополнительной информации о сбегающих замыканиях см. Сбегающие замыкания.

Списки захвата

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

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

Записи в списке захвата инициализируются при создании замыкания. Для каждой записи в списке захвата, константа инициализируется со значением константы или переменной, которая имеет то же имя в окружающей области. Например, в коде ниже, a включено в список захвата, а b нет, и это приводит к их различному поведению.

var a = 0

var b = 0

let closure = { [a] in

    print(a, b)

}

a = 10

b = 10

closure()

// Выведет "0 10"

Здесь и переменная в окружающей области и константа в области замыкания имеют имя a, но есть только одна переменная с именем b. Когда создается замыкание, a во внутренней области инициализируется со значением a внешней области, но их значения как-то особенно не объединены. Это означает, что изменение величины a во внешней области не влияет на величину a во внутренней области, а также изменение a внутри замыкания не влияет на величину a вне замыкания. В отличие от этого, существует только одна переменная с именем b - это b во внешней области, так что изменения внутри и снаружи замыкания видны в обоих местах.

Этого различия не видно, когда тип захваченной переменной имеет ссылочную семантику. Например, в коде ниже переменная во внешней области и константа во внутренней области имеют имя x:, но они относятся к одному объекту из-за ссылочной семантики.

class SimpleClass {

    var value = 0

}

var x = SimpleClass()

var y = SimpleClass()

let closure = { [x] in

    print(x.value, y.value)

}

x.value = 10

y.value = 10

closure()

// Выведет "10 10"

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

myFunction { print(self.title) }                    // strong захват

myFunction { [weak self] in print(self!.title) }    // weak захват

myFunction { [unowned self] in print(self.title) }  // unowned захват

Кроме того, можно связать произвольное выражение с именованным значением в списке захвата. Выражение вычисляется, когда создается замыкание, а значение захватывается с заданной "силой". Например:

// Weak захват "self.parent" как "parent"

myFunction { [weak parent = self.parent] in print(parent!.title) }

Подробнее о выражениях замыкания см. Замыкания.  Подробнее о списках захвата см. Замена циклов сильных ссылок в замыканиях.

Грамматика выражений замыканий

closure-expression → {closure-signature opt statements}

‌closure-signature → capture-list opt closure-parameter-clause throws  opt function-result opt in

‌closure-signature → capture-list in

‌closure-parameter-clause → ( ) | (closure-parameter-list) | identifier-list

‌closure-parameter-list → closure-parameter | closure-parameter, closure-parameter-list

‌closure-parameter → closure-parameter-nametype-annotation opt

‌closure-parameter → closure-parameter-nametype-annotation...

‌closure-parameter-name → identifier

‌capture-list → [capture-list-items]

‌capture-list-items → capture-list-item capture-list-item, capture-list-items

‌capture-list-item → capture-specifier opt expression

‌capture-specifier → weak  | unowned | unowned(safe) |  unowned(unsafe)

Неявное выражение члена

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

.имя члена

Например:

var x = MyEnumeration.SomeValue

x = .AnotherValue

Грамматика неявного выражения члена

implicit-member-expression → .identifier

Выражение в скобках

Выражения в скобках состоят из выражения, окруженного круглыми скобками. Вы можете использовать круглые скобки для явного указания приоритета выполнения той или иной операци. Группирвка при помощи скобок никак не меняет типа выражения. Например, тип выражения в скобках (1) является Int.

Грамматика выражения в скобках

parenthesized-expression → (expression)

Выражение кортежа

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

(identifier 1: expression 1, identifier 2: expression 2, ...)

Выражение кортежа может иметь 0 выражений или может содержать два или более выражений. Если в скобках находится всего одно выражение, то это уже является выражением в скобках, а не кортежем.

Заметка

В Swift можно записывать как пустое выражение кортежа, так и пустой тип кортежа (). Поскольку Void является псевдонимом типа (), вы можете использовать его для записи пустого типа. Однако, как и все псевдонимы типов, Void всегда является типом - вы не можете использовать его для написания пустого выражения.

Грамматика выражения кортежа

tuple-expression → () | (tuple-element,tuple-element-list) tuple-element-list → tuple-element  | tuple-element,tuple-element-list tuple-element → expression  | identifier:expression

Выражение wildcard

Выражение с подстановочными символами используется для явного игнорирования значения во время присваивания. Например, при следующем присваивании 10 присваивается к x и 20 игнорируется:

(x, _) = (10, 20)

// x равно 10, а значение 20 игнорируется

Грамматика выражения wildcard

wildcard-expression → _

Выражение Key-Path

Выражение key-path относится к свойству или сабскрипту типа. Key-path используется для динамических программных задач, например таких как KVO.

Они имеют следующую форму:

\type name.path

Type name - имя конкретного типа, включая любые общие параметры, например, String, [Int] или Set<Int>.

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

Каждый из компонентов key-path может быть повторен столько раз, сколько нужно в любой последовательности.

Во время компиляции выражение key-path подменяется экземпляром класса KeyPath.

Для того, чтобы получить доступ к значению через key-path, передайте key-path в сабскрипт subscript(keyPath:), который доступен во всех типах. Например:

struct SomeStructure {

var someValue: Int

}

let s = SomeStructure(someValue: 12)

let pathToProperty = \SomeStructure.someValue

let value = s[keyPath: pathToProperty]

// value равно 12

Имя типа может быть пропущено в контексте, где тип может быть определен. Следующий код использует \.someProperty вместо \SomeClass.someProperty:

class SomeClass: NSObject {

@objc var someProperty: Int

init(someProperty: Int) {

self.someProperty = someProperty

}

}

let c = SomeClass(someProperty: 10)

c.observe(\.someProperty) { object, change in

// ...

}

Path (или путь) может содержать в себе несколько имен свойств, разделенных между собой точками, ссылаясь на свойство значения свойства. Этот код использует выражения key-path \OuterStructure.outer.someValue, для того, чтобы получить доступ к свойству someValue свойства outer структуры OuterStructure:

struct OuterStructure {

var outer: SomeStructure

init(someValue: Int) {

self.outer = SomeStructure(someValue: someValue)

}

}

let nested = OuterStructure(someValue: 24)

let nestedKeyPath = \OuterStructure.outer.someValue

let nestedValue = nested[keyPath: nestedKeyPath]

// nestedValue равно 24

Path может включать в себя сабскрипты, использующие квадратные скобочки, если тип параметра соответсвует протоколу Hashable. Этот пример использует сабскрипты в key-path для того, чтобы получить доступ ко второму элементу массива:

let greetings = ["hello", "hola", "bonjour", "안녕"]

let myGreeting = greetings[keyPath: \[String].[1]]

// myGreeting равно 'hola'

Значение, используемое в сабскрипте, может именованное значение или литерал. Значения захватываются в key-path через семантику значения. Следующий код использует переменную index в обоих key-path выражениях, а так же и в замыкании, чтобы получить доступ к третьему элементу массива greetings. Когда мы модифицируем значение index, то выражение key-path все равно держит ссылку на третий элемент, в то время как замыкание уже использует новое значение индекса.

var index = 2

let path = \[String].[index]

let fn: ([String]) -> String = { strings in strings[index] }

print(greetings[keyPath: path])

// Выведет "bonjour"

print(fn(greetings))

// Выведет "bonjour"

// Присваивание 'index' нового значения не имеет никакого эффекта на 'path'

index += 1

print(greetings[keyPath: path])

// Выведет "bonjour"

// Так как 'fn' закрывает 'index', то оно использует новое его новое значение

print(fn(greetings))

// Выведет "안녕"

Path может использовать опциональную последовательность и принудительно извлеченные опционалы. Этот код использует опциональную последовательность в выражении key-path, чтобы получить значение свойства имеющего тип опциональной строки:

let firstGreeting: String? = greetings.first

print(firstGreeting?.count as Any)

// Выведет "Optional(5)"

// Do the same thing using a key path.

let count = greetings[keyPath: \[String].first?.count]

print(count as Any)

// Выведет "Optional(5)"

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

let interestingNumbers = ["prime": [2, 3, 5, 7, 11, 13,

15],

"triangular": [1, 3, 6, 10, 15, 21, 28],

"hexagonal": [1, 6, 15, 28, 45, 66, 91]]

print(interestingNumbers[keyPath: \[String: [Int]].["prime"]] as Any)

// Выведет "Optional([2, 3, 5, 7, 11, 13, 15])"

print(interestingNumbers[keyPath: \[String: [Int]].["prime"]![0]])

// Выведет "2"

print(interestingNumbers[keyPath: \[String: [Int]].["hexagonal"]!.count])

// Выведет "7"

print(interestingNumbers[keyPath: \[String: [Int]].["hexagonal"]!.count.bitWidth])

// Выведет “64"

Для более подробной информации по использованию key-path в коде, который взаимодействует с Objective-C APIs, смотрите Using Objective-C Runtime Features in Swift. Для более подробной информацией по KVC и KVO смотрите Key-Value Coding Programming Guide и Key-Value Observing Programming Guide.

Грамматика выражения Key-Path

key-path-expression → \ type opt . key-path-components

key-path-components → key-path-component | key-path-component . key-path-components

key-path-component → identifier key-path-postfixes opt | key-path-postfixes

key-path-postfixes → key-path-postfix key-path-postfixes opt

key-path-postfix → ? | ! | [ function-call-argument-list ]

Выражение селектора

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

#selector(имя метода)

#selector(getter: имя свойства)

#selector(setter: имя свойства)

Имя метода и имя свойства должны быть ссылкой на метод или свойство, который во время выполнения будут доступны в Objective-C. Значение выражения селектора является экземпляром типа Selector. Например:

class SomeClass: NSObject {

let property: String

@objc(doSomethingWithInt:)

func doSomething(_ x: Int) { }

init(property: String) {

self.property = property

}

}

let selectorForMethod = #selector(SomeClass.doSomething(_:))

let selectorForPropertyGetter = #selector(getter: SomeClass.property)

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

Имя метода может содержать скобки для группировки, также как оператор as содержит их для устранения неоднозначности между методами, которые разделяют имя, но имеют разные типы сигнатур. Например:

extension SomeClass {

@objc(doSomethingWithString:)

func doSomething(_ x: String) { }

}

let anotherSelector = #selector(SomeClass.doSomething(_:) as (SomeClass) -> (String) -> Void)

Поскольку селектор создается во время компилляции, а не во время выполнения, компиллятор может проверить существование  метода и то, что метод передается Objective-C.

Заметка

Несмотря на то, что имя метода и имя свойства являются выражением, они никогда не оцениваются.

Дополнительные сведения об использовании селекторов в коде на Swift, которые взаимодействуют с API-интерфейсами Objective-C, см. в разделе Особенности исполнения Objective-C в Swift.

Грамматика выражения селектора

selector-expression → #selector ( expression )

‌selector-expression → #selector ( getter: expression )

‌selector-expression → #selector ( setter: expression )

Выражение значения пути по ключу

Выражения пути по ключу позволяют вам получить доступ к строке, которая относится к свойству в Objective-C для использования в KVC и в KVO API's.

#keyPath(имя свойства)

Имя свойства должно ссылаться на свойство, которое доступно во время исполнения в Objective-C. Во время компилляции выражение пути отображается в виде строкового литерала. Например:

class SomeClass: NSObject {

@objc var someProperty: Int

init(someProperty: Int) {

self.someProperty = someProperty

}

}

let c = SomeClass(someProperty: 12)

let keyPath = #keyPath(SomeClass.someProperty)

if let value = c.value(forKey: keyPath) {

print(value)

}

// Выведет "12"

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

extension SomeClass {

func getSomeKeyPath() -> String {

return #keyPath(someProperty)

}

}

print(keyPath == c.getSomeKeyPath())

// Выведет"true"

Так как путь создается во время компилляции, а не во время исполнения, компиллятор может проверить, что свойство существует и открыто для Objective-C исполнения.

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

Заметка

Несмотря на то, что имя свойства является выражением, оно никогда не оценивается.

Грамматика выражения значения пути по ключу

key-path-expression → #keyPath ( expression )

Постфиксные выражения

Постфиксные выражения получаются путем применения постфиксного оператора или другого постфиксного синтаксиса к выражению. Синтаксически, каждое первичное выражение - это постфиксное выражение.

Для получения информации о поведении этих операторов см. Базовые операторы и Продвинутые операторы.

Для получения информации о предоставляемых стандартной библиотекой Swift операторах см. Operator Declarations.

Грамматика постфиксных выражений

postfix-expression → primary-expression

postfix-expression → postfix-expressionpostfix-operator

postfix-expression → function-call-expression

postfix-expression → initializer-expression

postfix-expression → explicit-member-expression

postfix-expression → postfix-self-expression

postfix-expression → dynamic-type-expression

postfix-expression → subscript-expression

postfix-expression → forced-value-expression

postfix-expression → optional-chaining-expression

Выражение вызова функции

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

имя функции(значение аргумента №1, значение аргумента №2)

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

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

имя функции(имя аргумента №1: значение аргумента №1,

имя аргумента №2: значение аргумента №2)

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

// someFunction принимает Int и замыкание в качестве своих аргументов

someFunction(x: x, f: {$0 == 13})

someFunction(x: x) {$0 == 13}

Если последующее замыкание является единственным аргументом функции, круглые скобки могут быть опущены.

// someFunction takes a closure as its only argument

myData.someMethod() {$0 == 13}

myData.someMethod {$0 == 13}

Грамматика выражения вызова функции

function-call-expression → postfix-expression function-call-argument-clause

function-call-expression → postfix-expression parenthesized-expressionopt trailing-closure

function-call-argument-clause → ( ) | (function-call-argument-list)

function-call-argument-list → function-call-argument | function-call-argument ,function-call-argument-list

function-call-argument → expression | identifier : expression

function-call-argument → operator | identifier : operator

trailing-closure → closure-expression

Выражение инициализатора

Выражение инициализатора обеспечивает доступ к инициализатору типа. Имеет следующий вид:

выражение.init(аргументы инициализатора)

Вы используете выражение инициализатора в выражение вызова функции для инициализации нового экземпляра типа. Вы также использовуете выражение инициализатора для делегирования инициализатору суперкласса.

class SomeSubClass: SomeSuperClass {

override init() {

// инициализация подкласса

super.init()

}

}

Как и функции, инициализатор может быть использован в качестве значения. Например:

// Аннотация типов обязательна, так как String имеет несколько инициализаторов.

let initializer: (Int) -> String = String.init

let oneTwoThree = [1, 2, 3].map(initializer).reduce("", combine: +)

print(oneTwoThree)

// Выведет "123"

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

let s1 = SomeType.init(data: 3) // Действительный вариант

let s2 = SomeType(data: 1) // Действительный вариант

let s3 = someValue.dynamicType.init(data: 7) // Действительный вариант

let s4 = someValue.dynamicType(data: 5) // Ошибка

Грамматика выражения инициализатора

initializer-expression → postfix-expression.init initializer-expression → postfix-expression.init(argument-names)

Явное выражение члена

Явное выражение члена позволяет получить доступ к членам именованного типа, кортежа, или модуля. Оно состоит из периода (.) между элементом и идентификатором его члена.

выражение.имя члена

Члены именованного типа названы как часть объявления типа или расширения. Например:

class SomeClass {

var someProperty = 42

}

let c = SomeClass()

let y = c.someProperty // доступ к члену

Члены кортежа неявно названы интеджерами в порядке их появления, начиная с нуля. Например:

var t = (10, 20, 30)

t.0 = t.1

// Теперь t равно (20, 20, 30)

Члены модуля получают доступ к объявлению топ-уровня этого модуля.

Типы, объявленные с атрибутом dynamicMemberLookup, включают элементы, которые просматриваются во время выполнения, как описано в Атрибутах.

Для того, чтобы различать методы или инициализаторы, чьи имена отличаются только именами своих аргументов, возьмите имена аргументов в скобки, и после каждого имени аргумента поставьте двоеточие (:). Поставьте подчеркивание (_) для аргумента без имени. Для того, чтобы различать перегруженные методы, используйте аннотацию типа. Например:

class SomeClass {

func someMethod(x: Int, y: Int) {}

func someMethod(x: Int, z: Int) {}

func overloadedMethod(x: Int, y: Int) {}

func overloadedMethod(x: Int, y: Bool) {}

}

let instance = SomeClass()

let a = instance.someMethod // Двусмысленно

let b = instance.someMethod(x:y:) // Недвусмысленно

let d = instance.overloadedMethod // Двусмысленно

let d = instance.overloadedMethod(x:y:) // Двусмысленно

let d: (Int, Bool) -> Void = instance.overloadedMethod(x:y:) // Недвусмысленно

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

let x = [10, 3, 20, 15, 4]

.sorted()

.filter { $0 > 5 }

.map { $0 * 100 }

Грамматика явного выражения члена

explicit-member-expression → postfix-expression.decimal-digits explicit-member-expression → postfix-expression.identifiergeneric-argument-clauseopt explicit-member-expression → postfix-expression.identifier(argument-names) argument-names → argument-name argument-namesopt argument-name → identifier:

Постфиксное выражение Self

Постфиксное выражение self состоит из выражения или имени типа, сразу за которым следует .self. Выглядит вот так:

выражение.self

тип.self

Первый вариант оценивает значение выражения. Например, x.self принимает значение x.

Второй вариант определяет значение типа. Используйте эту форму для получения доступа к типу в качестве значения. Например, поскольку SomeClass.self принимает значение самого типа SomeClass, вы можете передать его функции или методу, который принимает аргумент верхнего уровня.

Грамматика выражения self

postfix-self-expression → postfix-expression.self

Выражение сабскрипта

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

выражение[выражения индекса]

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

Информацию об объявлениях подстрочных индексов см. в Объявление протокола сабскрипта.

Грамматика выражения санскрита

subscript-expression → postfix-expression [ expression-list ]

Выражения принудительного значения

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

выражение!

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

Развернутое значение выражения принудительного значения может быть изменено, либо через изменение самого значения, или путем его присвоения одному из членов-значения. Например:

var x: Int? = 0

x! += 1

// x теперь 1

var someDictionary = ["a": [1, 2, 3], "b": [10, 20]]

someDictionary["a"]![0] = 100

// someDictionary теперь ["b": [10, 20], "a": [100, 2, 3]]

Грамматика принудительного значения

forced-value-expression → postfix-expression!

Выражение опциональной цепочки

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

выражение?

Постфиксный оператор ? из выражения делает выражение опциональной цепочки без изменения значения выражения.

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

Если постфиксное выражение, содержащее выражение опциональной цепочки, вложено в другие постфиксные выражения, только самое внешнее выражение возвращает опциональный тип. В приведенном ниже примере, когда c не nil, его значение разворачивается и используется для вычисления .property, значение которого используется для вычисления .performAction(). Все выражение c?.property.performAction() имеет значение опционального типа.

var c: SomeClass?

var result: Bool? = c?.property.performAction()

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

var result: Bool? = nil

if let unwrappedC = c {

result = unwrappedC.property.performAction()

}

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

func someFunctionWithSideEffects() -> Int {

return 42 // фактически никаких побочных эффектов не имеет

}

var someDictionary = ["a": [1, 2, 3], "b": [10, 20]]

someDictionary["not here"]?[0] = someFunctionWithSideEffects()

// someFunctionWithSideEffects не вычисляется

// someDictionary все еще ["a": [1, 2, 3], "b": [10, 20]]

someDictionary["a"]?[0] = someFunctionWithSideEffects()

// someFunctionWithSideEffects вычисляется, возвращается 42

// someDictionary теперь равно ["a": [42, 2, 3], "b": [10, 20]]

Грамматика выражения опциональной последовательности

optional-chaining-expression → postfix-expression?

Инструкции

В Swift существуют три вида инструкций: простые инструкции, инструкции управления компилятором, а также инструкции управления потоком. Простые инструкции являются наиболее распространенными и состоят либо из выражения или объявления. Инструкции управления компилятором позволяют программе изменять аспекты поведения компилятора, включают в себя конфигурацию сборки и инструкции управления строкой.

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

Точка с запятой (;) может использоваться по желанию после любой инструкции и используется для разделения нескольких инструкций, если они находятся в одной и той же строке.

Грамматика инструкций

statement → expression;opt statement → declaration;opt statement → loop-statement;opt statement → branch-statement;opt statement → labeled-statement;opt statement → control-transfer-statement;opt statement → defer-statement;opt statement → do-statement:opt statement → compiler-control-statement statements → statement statements opt

Инструкции цикла

Инструкции цикла позволяют блоку кода выполняться несколько раз, в зависимости от условий, указанных в цикле. В Swift есть три инструкции цикла: инструкция for-in, инструкция while, и инструкция repeat-while.

Управление потоком в инструкции цикла может быть изменено инструкцией break и continue и более подробно обсуждается в "Инструкция Break" и "Инструкция Continue" ниже.

Грамматика инструкции цикла

loop-statement → for-in-statement

loop-statement → while-statement

loop-statement → repeat-while-statement

Инструкция for-in

Инструкция for-in позволяет блоку кода выполняться один раз для каждого элемента в коллекции (или любого типа), соответствующего протоколу Sequence.

Выглядит следующим образом:

for item in collection{

выражения

}

Метод makeIterator() вызывается на выражение коллекции для получения значения итератора типа - это тип, который соответствует протоколу IteratorProtocol. Программа начинает выполнение цикла, вызывая метод next() в потоке. Если возвращенное значение не nil, оно вписывается под шаблон элемента, программа выполняет инструкции, а затем переносит выполнение на начало цикла. В противном случае программа не выполняет инструкции присваивания или исполнения и заканчивает выполнение иструкции for-in.

Грамматика инструкции for-in

for-in-statement → for caseopt pattern in expression where-clause opt code-block

Инструкция while

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

Инструкция while выглядит следующим образом:

while условие{

выражения

}

Инструкция while выполняется следующим образом:

Происходит оценка состояния. Если true, то выполнение переходит к шагу 2. Если false, то программа завершается выполнением инструкции while.

Программа выполняет инструкции, и выполнение программы возвращается к шагу 1.

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

Значение условия должно иметь тип, соответствующий протоколу Boolean. Условие может быть также объявлением опциональной привязки, как это описано Привязка опционалов.

Грамматика инструкции while

while-statement → while condition-clause code-block condition-list → condition | condition,condition-list condition → expression | availability-condition | case-condition | optional-binding-condition case-condition → case patterninitializer optional-binding-condition → let pattern initializer | var pattern initializer

Инструкция repeat-while

Инструкция repeat-while позволяет блоку кода выполняться один или несколько раз, до тех пор, пока условие остается верным.

Выглядит следующим образом:

repeat {

выражения

} while условие

Инструкция repeat-while выполняется следующим образом:

Программа выполняет инструкции, и выполнение переходит к шагу 2.

Происходит оценка условия.

Если true, выполнение возвращается к шагу 1. Если false, то программа завершает выполнение инструкции repeat-while.

Поскольку значение условия вычисляется после того, как выполняются инструкции, инструкции в инструкции repeat-while выполняются по крайней мере один раз.

Значение условия должно иметь тип, соответствующий протоколу Boolean. Условие может быть также опциональным связывающим объявлением, см. Привязка опционалов.

Грамматика repeat-while инструкции

repeat-while-statement → repeat code-block while expression

Инструкции ветвления

Инструкции ветвления позволяют программе выполнять определенные части кода в зависимости от значения одного или нескольких условий. Значения условий указываются в управлении инструкции ветвления и, более того, в непосредственно в ветке указывается код, который должен выполниться. В Swift есть три инструкции ответвления: инструкция if, инструкция guard, инструкция switch.

Управление потоком в инструкции if или инструкции switch может быть изменено инструкцией break и рассматривается в разделе Инструкция break ниже.

Грамматика инструкций ветвления

branch-statement → if-statement

branch-statement → guard-statement

branch-statement → switch-statement

Инструкция if

Инструкция if используется для выполнения кода, на основе оценки одного или нескольких условий.

Существуют две основные формы инструкции if. Для каждой из них требуются открывающиеся и закрывающиеся фигурные скобки.

if условие {

выражения

}

Второй вариант формы инструкции if дает дополнительное условие else (вводимое ключевым словом else) и используется для выполнения одной части кода, когда условие true, и другой части кода, когда это же условие false. Когда есть только одно условие else, инструкция if выглядит следующим образом:

if условие {

выражения для исполнения, если условие true

} else {

выражения для исполнения, если условие false

}

Условие else инструкции if может содержать другую инструкцию if для проверки более, чем одного условия. Инструкция if, соединенная друг с другом таким образом, будет выглядеть вот так:

if условие 1 {

выражения для исполнения, если условие 1 равно true

} else if условие 2 {

выражения для исполнения, если условие 2 равно true

} else {

выражения для исполнения, если условие 1 и 2 равны false

}

Значение любого условия в инструкции if должно иметь тип, соответствующий протоколу Boolean. Условие может быть также объявлением опциаональной привязки, см. Привязка опционалов.

Грамматика инструкции if

if-statement → if condition-clause   code-block  else-clause opt else-clause → else code-block  |  else if-statement

Инструкция guard

Инструкция guard используется для перевода контроля программы из области видимости, если одно или несколько условий не выполняются.

Выглядит следующим образом:

guard условие else {

выражения для исполнения, если условие false

}

Значение любого состояния в инструкции guard должно иметь тип, соответствующий протоколу Boolean. Условие может быть также объявлением опциональной привязки, см. Привязка опционалов.

Любые константы или переменные, которым присвоено значение из объявления привязки опционалов в условии инструкции guard, могут быть использованы для остальной части области видимости инструкции guard.

Требуется условие else инструкции guard, и должно либо вызывать функцию, отмеченную атрибутом noreturn или передать программное управление за пределы области видимости инструкции guard, используя одну из следующих инструкций:

return 

break

continue

throw

Инструкции передачи управления рассматриваются ниже в разделе "Операторы передачи управления". Для получения дополнительной информации о функциях с атрибутом noreturn, см. Функции, не возвращающие значения.

Грамматика инструкции guard

guard-statement → guard condition-clause else code-block

Инструкция switch

Инструкция switch позволяет определенным блокам кода выполняться в зависимости от значения контрольного выражения.

Инструкция switch имеет следующий вид:

switch контрольное выражение {

case шаблон 1

выражения

case шаблон 2 where условие:

выражения

case шаблон 3 where условие:

case шаблон 4 where условие:

выражения

default:

выражения

}

Контрольное выражение инструкции switch вычисляются и сравниваются с шаблонами, указанными в каждом конкретном случае (кейсе). Если найдено совпадение, программа выполняет инструкции, перечисленные в теле этого кейса. Тело каждого кейса не может быть пустым. В результате, вы должны включить, по меньшей мере, одну инструкцию, идущую после знака двоеточия (:) для каждого отдельного кейса. Используйте только одну инструкцию break, если не собираетесь выполнять какой-либо код в теле совпавшего кейса.

Значения выражения вашего кода могут очень гибко разветвляться. Например, в дополнение к значениям скалярных типов, таким как Int и Character, ваш код может разветвляться на значения любого типа, в том числе числа с плавающей точкой, строки, кортежи, экземпляры пользовательских классов и опционалы. Значение контрольного выражения может даже быть сопоставлено со значением кейса в перечислении и проверяться для включения в указанный диапазон значений. Подробнее о различных типах этих значений инструкции switch вы можете прочитать в Управление потоком в разделе Switch .

Кейс switch может опционально содержать условие where после каждого шаблона. Условие where вводится с помощью ключевого слова where, за которым следует выражение, и используется для введения дополнительного условия до шаблона, в случае если он соответствует выражению управления. Если есть условие where, инструкции внутри соответствующего кейса выполняются только в том случае, если значение контрольного выражения совпадает с одним из шаблонов кейса и выражения, в котором условие оценивается как true. Например, в приведенном ниже примере, контрольное выражение будет соответствовать кейсу, только если оно будет кортежем, содержащем два элемента одного и того же значения, такие как (1, 1).

case let (x, y) where x == y:

Как показано в примере выше, шаблоны в кейсе также могут связать константы, используя ключевое слово let (они также могут связывать переменные с помощью ключевого слова var). На эти константы (или переменные) затем могут ссылаться в соответствующем условии where и в остальной части кода в области данного кейса. Тем не менее, если кейс содержит несколько шаблонов, соответствующих выражению управления, то ни один из этих шаблонов не может содержать константу или привязки переменных.

Инструкция switch может также включать в себя дефолтный кейс, вводимый ключевым словом default. Код в дефолтное кейсе выполняется только, если другие кейсы не совпадают с контрольным выражением. Инструкция switch может включать в себя только один дефолтный кейс, и располагаться он должен в конце инструкции switch.

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

Инструкция Switch должна быть исчерпывающей

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

Выполнение неявно не “проваливается” через кейсы

После того, как код в совпавшем кейсе завершит выполнение, программа выходит из инструкции switch. Выполнение программы не продолжается и не «проваливается» к следующему кейсу или дефолтному кейсу. Тем не менее, если вы хотите продолжить выполнение одного кейса за другим, явно включите инструкцию fallthrough, который просто состоит из ключевого слова fallthrough, в кейсе, с которого вы хотите продолжить выполнение. Для получения дополнительной информации об инструкции fallthrough см. раздел "Инструкция Fallthrough" ниже.

Грамматика инструкции switch

switch-statement → switch expression{switch-casesopt} switch-cases → switch-case switch-cases opt switch-case → case-label statements  |  default-label statements case-label → case case-item-list: case-item-list → pattern where-clauseopt  |  pattern where-clauseopt,case-item-list default-label → default: where-clause → where where-expression where-expression → expression

Маркированная инструкция

Вы можете добавить префикс ярлыка инструкции к инструкции цикла, к инструкции if или к инструкции switch, который состоит из имени маркера, который следует непосредственно за двоеточием (:). Используйте маркеры инструкции с инструкциими break и continue, чтобы явно заявить о том, как вы хотите изменить управление потоком в инструкции цикла или инструкции switch, как это описано в "Инструкции Break" и "Инструкции Continue" .

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

О том как использовать маркеры инструкции см. Маркированные инструкции.

Грамматика маркированных инструкций

labeled-statement → statement-label loop-statement

labeled-statement →statement-label if-statement

labeled-statement →statement-label switch-statement

labeled-statement →statement-label do-statement

statement-label → label-name: label-name → identifier

Операторы передачи управления

Операторы передачи управления могут изменять порядок выполнения кода в вашей программе, передавая управление программой от одного куска кода к другому. В Swift есть пять инструкций передачи управления: инструкция break, инструкция continue, инструкция fallthrough, инструкция return и инструкция throw.

Грамматика инструкции передачи управления

control-transfer-statement → break-statement

control-transfer-statement → continue-statement

control-transfer-statement → fallthrough-statement

control-transfer-statement → return-statement

control-transfer-statement → throw-statement

Инструкция break

Инструкция break заканчивает выполнение программы цикла, инструкции if, или инструкции switch. Инструкция break может состоять только из ключевого слова break, или может состоять из ключевого слова break, за которым следует имя маркера инструкции, как показано ниже.

break

break имя ярлыка

Когда за инструкцией break следует имя маркера инструкции, он заканчивает выполнение программы цикла, инструкции if или инструкции switch, названного этим маркером.

Когда за оператором break не следует имя маркера инструкции, он заканчивает выполнение программы инструкции switch или инструкции самого внутреннего цикла, в котором находится. Вы не можете использовать немаркированную инструкция break для того, чтобы вырваться из инструкции if.

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

Подробнее см. "Оператор Break" и Маркированные инструкции в Управление потоком.

Грамматика инструкции Break

break-statement → break label-name opt

Инструкция continue

Инструкция continue заканчивает выполнение программы текущей итерации инструкции цикла, но не останавливает выполнение инструкции цикла. Инструкция continue может состоять только из ключевого слова continue, или может состоять из ключевого слова continue, за которым следует имя маркера инструкции, вот так:

continue

continue имя ярлыка

Когда за оператором continue следует имя маркера инструкции, оно заканчивает выполнение программы текущей итерации инструкции цикла, названного этим маркером.

Когда за оператором continue не следует имя маркера инструкции, оно заканчивает выполнение программы текущей итерации самого внутреннего инструкции цикла, в котором находится.

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

В инструкции for инкрементирующее выражение по-прежнему оценивается после того, как будет выполнена инструкция continue, так как инкрементирующее выражение вычисляется после выполнения тела цикла.

Подробнее см.  Оператор Continue и Маркированные инструкции в Управление потоком.

Грамматика инструкции Continue

continue-statement → continue label-name opt

Инструкция fallthrough

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

Инструкция fallthrough может появиться в любом месте внутри инструкции switch, а не только в качестве последней инструкции блока кейса, но он не может быть использован в финальном блоке кейса. Также он не может передавать управление блоку кейса, шаблон которого содержит значение связывающих шаблонов.

Подробнее см.  "Операторы передачи управления" в Управление потоком.

Грамматика инструкции fallthrough

fallthrough-statement → fallthrough

Инструкция return

Инструкция return встречается в теле функции или определения метода и благодаря ему выполнение программы возвращается к вызову функции или метода. Выполнение программы продолжается сразу после вызова функции или метода.

Инструкция return может состоять только из ключевого слова return, или из ключевого слова return, за которым будет следовать выражение, как показано ниже:

return

return выражение

Когда за инструкцией return следует выражение, значение выражения возвращают к вызову функции или метода. Если значение выражения не совпадает со значением возвращаемого типа, объявленного в функции или в объявлении метода, то значение выражения преобразуется в тип возвращаемого значения, прежде чем он будет возвращен к вызывающей функции или методу.

Заметка

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

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

Грамматика инструкции return

return-statement → return expression opt

Инструкция throw

Инструкция throw встречается в теле генерирующей ошибку функции или метода, или в теле выражения замыкания, тип которого отмечен ключевым словом throws.

Благодаря инструкции throw программа завершает выполнение текущей области и начинает распространение ошибок в области видимости. Сгенерированная ошибка продолжает распространяться, пока она не будет обрабатана условием catch инструкции do.

Инструкция throw состоит из ключевого слова throw, за которым следует выражение, как показано ниже:

    throw выражение

Значение выражения должно иметь тип, соответствующий протоколу ErrorProtocol.

Подробнее см. "Передача ошибки с помощью генерирующей функции" в разделе Обработка ошибок.

Грамматика инструкции throw

throw-statement → throw expression

Инструкция defer

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

Инструкция defer имеет следующий вид:

defer {

выражения

}

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

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

func f() {

defer { print("First") }

defer { print("Second") }

defer { print("Third") }

}

f()

// Выведет "Third"

// Выведет "Second"

// Выведет "First"

Операторы в инструкции defer могут передавать управление программой за пределы инструкции defer.

Грамматика инструкции defer

defer-statement → defer code-block

Инструкция do

Инструкция do используется, чтобы ввести новую область и может содержать один или несколько условий catch, содержащих шаблоны, соответствующих определенным условиям обработки ошибок. Переменные и константы, объявленные в области видимости инструкции do, будут доступны только в пределах этой области.

Инструкция do в Swift похожа на фигурные скобки ({}) в C и используется для разделения блока кода, и не снижает производительность во время выполнения.

Инструкция do выглядит так:

do {

try выражение

инструкции

} catch шаблон 1 {

инструкции

} catch шаблон 2 where условие {

инструкции

}

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

Для того, чтобы убедиться, что ошибка обрабатывается, используйте условие catch с шаблоном, который подходит ко всем ошибкам, например шаблон (_). Если условие catch не указывает шаблон, то условие catch подходит и связывает любую ошибку локальной константы с именем error. Для получения дополнительной информации о шаблонах см. Шаблоны.

Подробнее об использовании инструкции do с условиями catch, см. Обработка ошибок.

Грамматика инструкции do

do-statement → do code-block catch-clauses opt catch-clauses → catch-clause catch-clauses opt catch-clause → catch pattern opt where-clause opt code-block

Инструкции управления компиляторами

Инструкции управления компиляторами позволяют программе изменять аспекты поведения компилятора. В Swift есть две инструкции управления компиляторами: инструкция сборки конфигурации и инструкция управления строкой.

Грамматика инструкций управления компилятором

compiler-control-statement → conditional-compilation-block

compiler-control-statement → line-control-statement

compiler-control-statement → diagnostic-statement

Блок условной компиляции

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

Каждый блок условной компиляции начинается с #if и заканчивается #endif. Простой блок условной компиляции выглядит так:

#if условие компиляции

выражения

#endif

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

Условной компиляции может включать в себя литералы логического типа true и false, идентификатор, используемый с флагом командной строки -D, или любую платформу или функцию тестирования версий языка, перечисленных в таблице ниже.

Условие платформы

Действительные аргументы

os()

macOS, iOS, watchOS, tvOS, Linux

arch()

i386, x86_64, arm, arm64

swift()

>= с последующим указанием номером версии

canImport()

Имя модуля

targetEnvironment()

simulator

Номер версии для функции тестирования языковой версии swift() состоит из major и minor числа, разделенных точкой (.). Между  >= и номером версии не должно быть пробелов.

Аргументом для условия canImport() является имя модуля, который может отсутствовать. Это условие проверяет, возможно ли импортировать модуль, но фактически не импортирует его. Если модуль присутствует, условие версии возвращает true; в противном случае он возвращает false.

Условие версии targetEnvironment() возвращает true, когда код скомпилирован для симулятора; в противном случае он возвращает false.

Заметка

Функция тестирования платформы arch(arm) не возвращает true для устройств ARM 64. Функция тестирования платформы arch(i386) возвращает true, если код скомпилирован для 32-битного iOS симулятора.

Вы можете комбинировать условие компиляции, используя логические операторы &&, ||, и ! и использовать круглые скобки для группировки.

Аналогично инструкции if, вы можете добавить несколько веток условий для проверки различных условных компиляций. Вы можете добавить любое количество дополнительных ветвей через условия #elseif. Вы также можете добавить конечную дополнительную ветвь, используя условие #else. Условная компиляция, содержащий несколько ветвей, выглядит вот так:

#if условие компиляции 1

инструкции для компиляции, если условие компиляции 1 будет true

#elseif условие компиляции 2

инструкции для компиляции, если условие компиляции 2 будет true

#else

инструкции для компиляции, если условие компиляции 1 и 2 будет false

#endif

Заметка

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

Грамматика блока условной компиляции

conditional-compilation-block → if-directive-clause elseif-directive-clausesopt statementsopt

if-directive-clause → if-directive compilation-condition statementsopt

elseif-directive-clauses→ elseif-directive-clause elseif-directive-clausesopt

elseif-directive-clause→ elseif-directive compilation-condition statementsopt

else-directive-clause → else-directivestatementsopt

if-directive → #if

‌elseif-directive → #elseif

‌else-directive → #else

‌endif-directive → #endif

compilation-condition → platform-condition

‌compilation-condition → identifier

‌compilation-condition → boolean-literal

‌compilation-condition → ( compilation-condition )

‌compilation-condition → ! compilation-condition

‌compilation-condition → compilation-condition && compilation-condition

‌compilation-condition → compilation-condition || compilation-condition

‌platform-condition → os ( operating-system )

‌platform-condition → arch ( architecture )

‌platform-condition → swift ( >= swift-version )

‌‌platform-condition → canImport ( module-name )

‌‌platform-condition → targetEnvironment ( environment )

operating-system → macOS | iOS | watchOS | tvOS

‌architecture → i386 | x86_64 | | arm | arm64

‌swift-version → decimal-digits swift-version-continuationopt

swift-version-continuation → . decimal-digits swift-version-continuationopt

module-name → identifier

environment → simulator

Инструкция управления строкой

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

Инструкция управления строкой имеет следующий вид:

#sourceLocation(file: имя файла, line: номер строки)

#sourceLocation

Инструкция управления строкой изменяет значения #line и #file выражения литералов, начиная со строки кода после инструкции управления строкой. Номер строки изменяет значение #line и может быть любым целым числом больше нуля. Имя файла изменяет значение #file и является строковым литералом.

Вторая инструкция управления строкой, #sourceLocation (), сбрасывает исходный код и имя файла обратно по умолчанию.

Грамматика инструкции управления строкой

line-control-statement → #sourceLocation ( file: file-name,line:line-number)

‌line-control-statement → #sourceLocation ( )

‌line-number → Целое число большее нуля

‌file-name → static-string-literal

Диагностический оператор времени компиляции

Оператор диагностики времени компиляции заставляет компилятор выделять ошибку или предупреждать во время компиляции. Диагностическая инструкция для компиляции имеет следующие виды:

#error("error message")

#warning("warning message")

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

Грамматика оператора диагностики времени компиляции

diagnostic-statement → #error ( diagnostic-message )

diagnostic-statement → #warning ( diagnostic-message )

diagnostic-message →static-string-literal

Условие доступности

Условие доступности используется в качестве условия инструкции if, while и guard для запроса доступности API во время выполнения, основываясь на аргументы указанных платформ.

Условие доступности выглядит так:

if #available (название платформы версия, ..., *) {

инструкции для исполнения, если API доступен

} else {

резервные инструкции на случай не доступности API

}

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

Условие доступности принимает список имен платформ и версий разделенных запятыми. Используйте iOS, OSX и watchOS в качестве названий платформ, и добавьте соответствующие номера версий к ним. Аргумент * является обязательным и указывает, что на любой другой платформе тело блока кода, охраняемого условием доступности, выполняется на минимальной deployment target (минимальная версия поддерживающая приложения), указанный в taget проекта.

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

Грамматика условий доступности

availability-condition → #available ( availability-arguments )

availability-arguments → availability-argument  |  availability-argument , availability-arguments

‌availability-argument → platform-name platform-version

‌availability-argument → *

‌platform-name → iOS  |  iOSApplicationExtension

‌platform-name → OSX  |  OSXApplicationExtension

‌platform-name → watchOS

‌platform-name → tvOS

‌platform-version → decimal-digits

‌platform-version → decimal-digits . decimal-digits

‌platform-version → decimal-digits . decimal-digits . decimal-digits

Объявления

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

В Swift большинство объявлений являются также определениями, в том смысле, что они реализуются или инициализируются в то же время, когда и объявляются. Тем не менее, поскольку протоколы не реализуют свои члены (элементы), большинство членов протокола являются только объявлениями. Так как различие не столь важно в Swift, а также для простого удобства, термин “объявления” используются и для объявлений и для определений.

Грамматика объявления

declaration → import-declaration

declaration → constant-declaration

declaration → variable-declaration

declaration → typealias-declaration

declaration → function-declaration

declaration → enum-declaration

declaration → struct-declaration

declaration → class-declaration

declaration → protocol-declaration

declaration → initializer-declaration

declaration → deinitializer-declaration

declaration → extension-declaration

declaration → subscript-declaration

declaration → operator-declaration

declarations → declaration declarations opt

Код верхнего уровня

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

Грамматика объявления верхнего уровня

top-level-declaration → statements opt

Блоки кода

Блок кода используется различными объявлениями и контролирующими структурами для объединения объявлений. Выглядит следующим образом:

{

инструкции

}

Инструкции внутри блока кода включают в себя объявления, выражения и другие виды инструкций и выполняются в порядке их появления в исходном коде.

Грамматика блока кода

code-block → { statements opt }

Объявление импорта

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

import модуль

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

import тип импорта модуль . имя символа

import модуль . подмодуль

Грамматика объявления импорта

import-declaration → attributes opt import import-kindopt import-path

import-kind → typealias | struct | class | enum | protocol | var | func

import-path → import-path-identifier | import-path-identifier . import-path

import-path-identifier → identifier | operator

Объявления константы

Объявления константы вводят в вашу программу именованное значение константы. Объявления константы объявляются с помощью ключевого слова let и выглядят следующим образом:

let имя константы: тип = выражение

Объявление константы устанавливает неизменную связь между именем константы и значением выражения инициализатора. После того, как значение константы установлено, оно не может быть изменено. Тем не менее, если константа инициализируется с объектом класса, сам объект может изменяться, а связь между именем константы и объектом, на который он ссылается, меняться не может.

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

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

let (firstNumber, secondNumber) = (10, 42)

В этом примере firstNumber - это именованная константа для значения 10, а secondNumber - это именованная константа для значения 42. Обе константы теперь можно использовать независимо друг от друга:

print("Первое число равно \(firstNumber).")

// Выведет "Первое число равно 10."

print("Второе число равно \(secondNumber).")

// Выведет "Второе число равно 42."

Тип аннотаций (: type) опционален в объявлении константы, когда тип имени константы можно вывести, как описано в разделе Вывод типа.

Чтобы объявить свойство типа константы, отметьте объявление модификатором объявления static. Свойства типа обсуждаются в Свойства типа.

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

Грамматика объявления константы

constant-declaration → attributes opt declaration-modifiers opt let pattern-initializer-list

pattern-initializer-list → pattern-initializer pattern-initializer , pattern-initializer-list

pattern-initializer → pattern initializer opt

initializer → = expression

Объявление переменных

Объявление переменной вводит в вашу программу именованное значение переменной и объявляется с помощью ключевого слова var.

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

Заметка

Вы можете также объявлять свойства в контексте объявления протокола, как описано в "Объявлении свойств протокола".

Вы можете переопределить свойство в подклассе, отметив объявление свойства подкласса модификатором объявления override, как описано в Переопределение.

Переменные хранения и переменные свойства хранения

Следующая форма объявляет переменные хранения или переменные свойств хранения:

var имя переменной: тип = выражение

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

Выражение инициализатора не может быть прописано в протоколе, а во всех других контекстах выражение инициализатора является опциональным. Тем не менее, если нет ни одного выражения инициализатора, то объявление переменной должно содержать явную аннотацию типа (: type).

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

Как ясно из названия, значение переменной хранения или переменной свойств хранения хранятся в памяти.

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

Следующая форма объявляет вычисляемую переменную или вычисляемое свойство:

var имя переменной: тип {

get {

инструкции

}

set(имя сеттера) {

инструкции

}

}

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

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

Имя сеттера и круглые скобки являются опциональными. Если вы предоставляете имя сеттера, оно используется в качестве имени параметра сеттера. Если вы не даете сеттеру имя, то имя параметра по умолчанию будет newValue, как описано в Сокращенный вариант объявления сеттера.

В отличие от именованного значения хранения и переменных свойств хранения, значение вычисляемого им именованного значения или вычисляемого свойства не сохраняются в памяти.

Для получения дополнительной информации см. Вычисляемые свойства.

Наблюдатели переменной хранения и наблюдатели свойств

Кроме того, вы можете объявить переменную хранения или свойство с наблюдателями willSet и didSet. Переменная хранения или свойство, объявленные с наблюдателями выглядят так:

var имя переменной: тип = выражение {

willSet(имя сеттера) {

инструкции

}

didSet(имя сеттера) {

инструкции

}

}

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

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

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

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

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

Имя сеттера и круглые скобки в условиях willSet и didSet являются опциональными. Если вы предоставляете имена сеттеров, они используются в качестве имен параметров наблюдателей willSet и didSet. Если вы не предоставляете имена сеттеров, дефолтное имя параметра наблюдателя willSet будет newValue и дефолтное имя параметра наблюдателя didSet будет oldValue.

Условие didSet является дефолтным, когда вы предоставляете условие willSet. Аналогичным образом, условие willSet является опциональным, если вы предоставляете условие didSet.

Для получения дополнительной информации см. Наблюдатели свойств.

Переменные свойства типа

Для того, чтобы объявить переменную свойства типа, отметьте объявление модификатором объявления static. Классы могут отметить вычисляемые свойства типа модификатором объявлении class, вместо того, чтобы позволить подклассам переопределить реализацию суперкласса. Свойства типа подробнее Свойства типа.

Заметка

В объявлении класса ключевое слово static имеет тот же эффект, что и отметка обоими модификаторами объявления class и final.

Грамматика объявления переменной

variable-declaration → variable-declaration-head pattern-initializer-list

variable-declaration → variable-declaration-head variable-name type-annotation code-block

variable-declaration → variable-declaration-head variable-name type-annotation getter-setter-block

variable-declaration → variable-declaration-head variable-name type-annotation getter-setter-keyword-block

variable-declaration → variable-declaration-head variable-name initializer willSet-didSet-block

variable-declaration → variable-declaration-head variable-name type-annotation initializeropt willSet-didSet-block

variable-declaration-head → attributes opt declaration-modifiersopt var

variable-name → identifier

getter-setter-block → code-block

getter-setter-block → { getter-clause setter-clauseopt }

getter-setter-block → { setter-clause getter-clause }

getter-clause → attributesopt get code-block

setter-clause → attributesopt set setter-nameopt code-block

setter-name → ( identifier )

getter-setter-keyword-block → { getter-keyword-clause setter-keyword-clauseopt }

getter-setter-keyword-block → { setter-keyword-clause getter-keyword-clause }

getter-keyword-clause → attributesopt get

setter-keyword-clause → attributesopt set

willSet-didSet-block → { willSet-clause didSet-clauseopt }

willSet-didSet-block → { didSet-clause willSet-clauseopt }

willSet-clause → attributesopt willSet setter-nameopt code-block

didSet-clause → attributesopt didSet setter-nameopt code-block

Объявление псевдонима типа

Объявление псевдонима типа вводит именованный псевдоним существующего типа в программу. Объявления псевдонима типа объявляются с помощью ключевого слова typealias и имеют следующий вид:

typealias имя = существующий тип

После того, как псевдоним типа объявлен, имя псевдонима может быть использовано вместо существующего типа везде в вашей программе. Существующий тип может быть именованным типом или составным типом. Псевдонимы типов не создают новые типы, они просто позволяют имени ссылаться на существующий тип.

Объявление псевдонима типа может использовать универсальные параметры для того, чтобы давать имена существующим типам. Например:

typealias StringDictionary<T> = Dictionary<String, T>

// Эти словари имеют один и тот же тип.

var dictionary1: StringDictionary<Int> = [:]

var dictionary2: Dictionary<String, Int> = [:]

Когда псевдоним типа объявлен с универсальными параметрами, то ограничения этих параметров должны точно совпадать с ограничениями параметров существующего типа. Например:

typealias DictionaryOfInts<Key: Hashable> = Dictionary<Key, Int>

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

Внутри объявления протокла псевдоним типа может дать более короткое, более удобное имя часто используемому типу. Например:

protocol Sequence {

associatedtype Iterator: IteratorProtocol

typealias Element = Iterator.Element

}

func sum<T: Sequence>(_ sequence: T) -> Int where T.Element == Int {

// ...

}

Без этого псевдонима, метод sum должен был бы ссылать на ассоциативный тип T.Iterator.Element, вместо T.Element.

Подробнее см. Объявление ассоциированного типа протокола.

Грамматика объявления псевдонима типа

typealias-declaration → attributesopt access-level-modifieropt typealias typealias-name typealias-assignment

typealias-name → identifier

typealias-assignment → = type

Объявление функции

Объявление функции вводит функцию или метод в вашу программу. Функция, объявленная в контексте класса, структуры, перечисления, или протокола, называется методом. Объявления функций осуществляются с помощью ключевого слова func и имеют следующий вид:

func имя функции (параметры) -> возвращаемый тип {

инструкции

}

Если функция имеет возвращаемый тип Void, возвращаемый тип может быть опущен следующим образом:

func имя функции (параметры) {

инструкции

}

Тип каждого параметра должен быть включен, так как он не может быть выведен. Хотя параметры функции являются по умолчанию константами, вы можете написать let перед именем параметра, чтобы подчеркнуть его поведение. Если написать перед именем параметра inout, параметр может быть изменен в области видимости функции. Параметры In-Out подробно обсуждаются в "Сквозные параметры", ниже.

Функции могут возвращать несколько значений, используя тип кортежа как тип возвращаемого значения функции.

Определение функции может появиться внутри другого объявления функции. Этот вид функции известен как вложенная функция.

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

Подробнее о вложенных функциях см. Вложенные функции.

Имена параметров

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

имя параметра: тип параметра

Параметр имеет локальное имя, которое используется в теле функции, а также внешнее имя, которое используется в качестве ярлыка для аргумента при вызове метода. По умолчанию имена параметров также используются как ярлыки аргументов. Например:

func f(x: Int, y: Int) -> Int { return x + y }

f(x: 1, y: 2) // и x, и у имеют свой ярлык

Вы можете переопределить дефолтное поведение для лейблов аргументов, используя одну из форм:

ярлык аргумента имя параметра: тип параметра

_ имя параметра: тип параметра

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

Подчеркивание (_) перед именем параметра запрещает использование ярлыка аргумента. Соответствующий аргумент не должен иметь никакого имени при вызове функции или метода.

func repeatGreeting(_ greeting: String, count n: Int) { /* Поприветствовать n раз */ }

repeatGreeting("Hello, world!", count: 2) //  count имеет ярлык, а greeting нет

Сквозные параметры

Сквозные параметры передаются следующим образом:

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

В теле функции копия изменяется.

Когда функция возвращается, значение копии присваивается исходному аргументу.

Такое поведение известно как “copy-in copy-out”  или вызов по результату значения. Например, когда вычисляемое свойство или свойство с наблюдателями передается в качестве сквозного параметра, его геттер вызывается как часть вызова функции и его сеттер вызывается как часть возврата функции.

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

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

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

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

func someFunction(a: inout Int) -> () -> Int {

return { [a] in return a + 1 }

}

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

func multithreadedFunction(queue: DispatchQueue, x: inout Int) {

// Сделайте локальную копию и скопируйте ее обратно.

var localX = x

defer { x = localX }

// Работайте с localX асинхронно, затем дождитесь возврата.

queue.async { someMutatingOperation(&localX) }

queue.sync {}

}

Подробнее см. Сквозные параметры.

Особые виды параметров

Параметры могут быть проигнорированы, принимают переменное число значений, а также обеспечивают дефолтные значения, используя следующие формы:

_ : тип параметра

имя параметра: тип параметра...

имя параметра: тип параметра = дефолтное значение аргумента

Параметр подчеркивание (_) явно игнорируется и не может быть доступен в теле функции.

Параметр с именем базового типа следуют сразу за троеточием (...) и понимается как вариативный параметр. Функция может иметь не более одного вариативного параметра. Вариативный параметр рассматривается как массив, который содержит элементы имени базового типа. Например, вариативный параметр Int... рассматривается как [Int]. Подробнее см. Вариативные параметры.

Параметр со знаком равенства (=) и выражением, стоящим после типа, понимается как имеющий дефолтное значение данного выражения. Данное выражение вычисляется, когда вызывается функция. Если параметр опущен при вызове функции, значение по умолчанию используется вместо него.

func f(x: Int = 42) -> Int { return x }

f()       // Корректно, использует дефолтное значение

f(x: 7)   // Корректно, использует предоставленное значение

f(7)      // Некорректно, отсутствует ярлык аргумента

Специальные виды методов

Методы перечисления или структуры, которые изменяют self, должны быть отмечены модификатором объявления mutating.

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

Методы, связанные с типом, а не с экземпляром типа, должны быть отмечены модификатором объявления static для перечислений и структур или модификатором объявления class для классов.

Генерирующие функции и методы

Функции и методы, которые могут генерировать ошибку, должны быть отмечены ключевым словом throws. Эти функции и методы известны, как генерирующие ошибку функции и методы. Выглядят они вот так:

func имя функции (парметры) throws -> возвращаемый тип {

инструкции

}

Вызовы генерирующей ошибки функции или метода должны быть завернуты в выражение try или try! (то есть, в области оператора try и try!).

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

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

Генерирующий метод не может переопределить негенерирующий метод, и генерирующий метод не может удовлетворить требование протокола для негенерирующего метода. Тем не менее, негенерирующий метод может переопределить генерирующий метод, и негенерирующий метод может удовлетворить требование генерирующего протокола.

Повторно генерирующие функции и методы

Функция или метод могут быть объявлены ключевым словом rethrows, чтобы указать, что они генерируют ошибку только тогда, когда один из их параметров функции генерирует ошибку. Эти функции и методы известны как повторно генерирующие функции и методы. Повторно генерирующие функции и методы должны иметь по крайней мере один параметр генерирующей функции.

func someFunction(callback: () throws -> Void) rethrows {

try callback()

}

Повторно генерирующая функция или метод могут содержать оператор throw только внутри условия catch. Это позволяет вызывать генерирующую функцию внутри блока do-catch и обрабатывать ошибки в условии catch, генерируя другую ошибку. В дополнение, условие catch должно обрабатывать только ошибки, сгенерированные одним из генерирующих параметров повторно генерирующей функции. Например,следующий код будет неправильным, потому что уловие catch будет обрабатывать ошибку, сгенерированную alwaysThrows().

func alwaysThrows() throws {

    throw SomeError.error

}

func someFunction(callback: () throws -> Void) rethrows {

   do {

      try callback()

      try alwaysThrows() // ошибка, так как alwaysThrows не является генерирующим параметром

   } catch {

      throw AnotherError.error

   }

}

Генерирующий метод не может переопределить повторно генерирующий метод, и генерирующий метод не может удовлетворить требование протокола для повторно генерирующего метода. Тем не менее, повторно генерирующий метод может переопределить генерирующий метод, и повторно генерирующий метод может удовлетворить требование протокола для генерирующего метода.

Функции, не возвращающие значения

Swift определяет тип Never, который является индикатором того, что функция не возвращает результата. Функции и методы с возвращаемым типом Never называются невозвращающими. Невозвращающие функции и методы либо являются источниками непоправимых ошибок, либо является источником начала работ, которые продолжаются до бесконечности. Это все значит, что код, который должен исполняться сразу после вызова, никогда отработан не будет. Генерирующие и генерирующие повторно функции могут перебросить выполнение программы в соответствующий catch блок, даже если они являются невозвращающими. Невозвращающие функции или методы могут быть вызваны для звершения работы else скобки инструкции guard.

Вы можете переопределить невозвращающий метод, но новый метод должен сохранить возвращаемый тип и поведение не возврата.

Грамматика объявления функции

function-declaration → function-head function-name generic-parameter-clauseopt function-signature function-bodyopt

‌function-head → attributesopt declaration-modifiersopt func

‌function-name → identifier | operator

‌function-signature → parameter-clause throwsopt function-resultopt

‌function-signature → parameter-clause rethrows function-resultopt

‌function-result →  -> attributesopt type

‌function-body → code-block

‌parameter-clause → () | ( parameter-list )

‌parameter-list → parameter | parameter , parameter-list

‌parameter → external-parameter-nameopt local-parameter-name type-annotation default-argument-clauseopt

parameter → external-parameter-nameopt local-parameter-name type-annotation

‌parameter → external-parameter-nameopt local-parameter-namet ype-annotation ...

‌external-parameter-name → identifier

‌local-parameter-name → identifier

‌default-argument-clause → = expression

Объявление перечисления

Объявление перечисления вводит именованный тип перечисления в вашу программу.

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

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

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

Как и структуры, но в отличие от классов, перечисления - это типы значений. Экземпляры перечисления копируются при присваивании переменным или константам, или при передаче в качестве аргументов вызова функции. Для получения дополнительной информации о типах значений см. Структуры и перечисления являются типами значений.

Вы можете расширить поведение типа перечисления через объявление расширения, подробнее "Объявление расширения", ниже.

Перечисления с кейсами любого типа

Следующая форма объявляет тип перечисления, содержащий кейсы перечисления любого типа:

enum имя перечисления: принятые протоколы {

case кейс перечисления 1

case кейс перечисления 2 (связанные значения типа)

}

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

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

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

enum Number {

   case integer(Int)

   case real(Double)

}

let f = Number.integer

// f является функцией типа (Int) -> Number

// Применяем функцию f для создания массива экземпляров Number с целочисленными значениями

let evenInts: [Number] = [0, 2, 4, 6].map(f)

Для получения дополнительной информации см. Связанные значения.

Индиректные перечисления

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

Чтобы включить индиректность для конкретного кейса перечисления, отметьте его модификатором объявления indirect.

enum Tree<T> {

    case empty

    indirect case node(value: T, left: Tree, right: Tree)

}

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

Кейс перечисления, отмеченный модификатором indirect, должен иметь связное значение. Перечисление, отмеченное модификатором indirect может содержать смесь кейсов, имеющих связные значения и кейсы, не имеющие их. Тем не менее, он не может содержать каких-либо кейсов, также отмеченных модификатором indirect.

Перечисления с кейсами типа исходного значения

Следующая форма объявляет тип перечисления, который содержит кейсы перечисления одного и того же базового типа:

enum имя перечисления: исходное значение типа, принятые протоколы {

case кейс перечисления 1 = исходное значение 1

case кейс перечисления 2 = исходное значение 2

}

В этой форме каждый конкретный блок кейса состоит из ключевого слова case, за которым следует один или более кейсов перечисления через запятую. В отличие от кейсов в первой форме, здесь каждый кейс имеет основное значение, называемое исходное значение того же базового типа. Тип этих значений задается в типе исходных значений и должен представлять интеджер, число с плавающей точкой, строку или символ. В частности, тип исходного значения должен соответствовать протоколу Equatable и одному из следующих  протоколов: IntegerLiteralConvertible для целочисленных литералов, FloatingPointLiteralConvertible для литералов с плавающей точкой, StringLiteralConvertible для строковых литералов, которые содержат любое количество символов, и ExtendedGraphemeClusterLiteralConvertible для строковых литералов, которые содержат только один символ. Каждый кейс должен иметь уникальное имя и быть присвоен уникальному исходному значению.

Если исходное значение установлено как Int и вы не присваиваете явно значение кейсам, они неявно присваиваются значениям 0, 1, 2, и так далее. Каждый неуказанный кейс типа Int неявно присваивается исходному значению, которое автоматически увеличивается от исходного значения предыдущего кейса.

enum ExampleEnum: Int {

   case a, b, c = 5, d

}

В приведенном выше примере, исходное значение ExampleEnum. A равно 0 и значение ExampleEnum.B равно 1. Так как значение ExampleEnum.C явно установлено на 5, то значение ExampleEnum.D автоматически увеличивается с 5 и будет 6.

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

enum GamePlayMode: String {

   case cooperative, individual, competitive

}

В приведенном выше примере, исходным значением GamePlayMode.cooperative является "cooperative", исходным значением GamePlayMode.individual является "individual", исходным значением GamePlayMode.competitive является "competitive".

Перечисления, которые имеют кейсы типа исходного значения, неявно соответствуют протоколу RawRepresentable, определенному в стандартной библиотеке Swift. В результате, у них есть свойство rawValue и проваливающийся инициализатор с подписью init?(rawValue: RawValue). Вы можете также использовать свойство rawValue для доступа к исходному значению кейса перечисления, как в ExampleEnum.B.rawValue. Вы можете также использовать исходное значение для того, чтобы найти соответствующий кейс, если таковой имеется, вызвав проваливающийся инициализатор перечисления, как в ExampleEnum(rawValue: 5), который возвращает опциональный кейс. Подробнее см. Исходные значения.

Доступ к кейсам перечисления

Для того, чтобы сослаться на кейс типа перечисления, используйте точку (.), как в EnumerationType.EnumerationCase. Когда тип перечисления можно вывести из контекста, вы можете опустить его (точка по-прежнему требуется), как описано в Синтаксисе перечисления и Неявное выражение члена.

Чтобы проверить значения кейсов перечисления, используйте оператор switch, как показано в Использование перечислений с инструкцией switch. Тип перечисления - шаблонный, а шаблоны кейсов перечисления находятся в блоках кейсов оператора switch - все это описано в Шаблон перечисления кейса.

Грамматика объявления перечислений

enum-declaration → attributesopt access-level-modifieropt union-style-enum

enum-declaration → attributesopt access-level-modifieropt raw-value-style-enum

union-style-enum → indirectopt enum enum-name generic-parameter-clauseopt type-inheritance-clauseopt { union-style-enum-membersopt }

union-style-enum-members → union-style-enum-member union-style-enum-membersopt

union-style-enum-member → declaration | union-style-enum-case-clause

union-style-enum-case-clause → attributesopt indirectopt case union-style-enum-case-list

union-style-enum-case-list → union-style-enum-case | union-style-enum-case , union-style-enum-case-list

union-style-enum-case → enum-case-name tuple-typeopt

enum-name → identifier

enum-case-name → identifier

raw-value-style-enum → enum enum-name generic-parameter-clauseopt type-inheritance-clause { raw-value-style-enum-members }

raw-value-style-enum-members → raw-value-style-enum-member raw-value-style-enum-membersopt

raw-value-style-enum-member → declaration raw-value-style-enum-case-clause

raw-value-style-enum-case-clause → attributesopt case raw-value-style-enum-case-list

raw-value-style-enum-case-list → raw-value-style-enum-case | raw-value-style-enum-case , raw-value-style-enum-case-list 

raw-value-style-enum-case → enum-case-name raw-value-assignmentopt

raw-value-assignment → = raw-value-literal

raw-value-literal → numeric-literal | static-string-literal | boolean-literal

Объявление структуры

Объявление структуры вводит именованный тип структуры в вашу программу. Объявление структуры объявляются с помощью ключевого слова struct и выглядит следующим образом:

struct имя структуры: принятые протоколы {

объявления

}

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

Структурные типы могут принимать любое количество протоколов, но не могут наследовать от классов, перечислений, или других структур.

Есть три способа создания экземпляра ранее объявленной структуры:

Вызовите один из инициализаторов, объявленных внутри структуры, как описано в Инициализаторы.

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

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

Процесс инициализации объявленных свойств структуры см. Инициализация.

Свойства экземпляра структуры можно получить через синтаксис: точка (.), как описано в разделе Доступ к свойствам.

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

Вы можете расширить поведение типа структур с объявлением расширения.

Грамматика объявления структур

struct-declaration → attributesopt access-level-modifieropt struct - struct-name  generic-parameter-clauseopt  type-inheritance-clauseopt  generic-where-clauseopt struct-body struct-name → identifier struct-body → { struct-membersopt } struct-members → struct-memberstruct-membersopt struct-member → declaration  |  compiler-control-statement

Объявление класса

Объявление класса вводит именованный классовый тип в вашу программу. Объявления класса объявляются с помощью ключевого слова class и выглядят следующим образом:

class имя класса: имя суперкласса, принятые протоколы {

объявления

}

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

Тип класса может наследовать только от одного родительского класса, его суперкласса, но может принять любое количество протоколов. Суперкласс идет первым после имени класса и двоеточия, за ним любые принятые протоколы. Универсальные классы могут наследовать от других универсальных и неуниверсальных классов, но неуниверсальный класс может наследовать только от других неуниверсальных классов. Когда вы пишете имя универсального суперкласса класса после двоеточия, вы должны указать полное имя этого универсального класса, в том числе его универсальный тип.

Как говорится в "Объявление инициализатора", классы могут иметь назначеные (designated) и пригодные (convenience ) инициализаторы. Назначенный инициализатор класса должен инициализировать все объявленные свойства класса и он должен сделать это до вызова любого из назначенных инициализаторов своего суперкласса.

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

Для того, чтобы подклассы реализовывали инициализатор суперкласса, отметьте инициализатор суперкласса модификатором объявления required. Реализация подкласса этого инициализатора также должна быть отмечена модификатором объявления required.

Хотя свойства и методы, объявленные в суперклассе, наследуются текущим классом, назначенные инициализаторы, объявленные в суперклассе, так не наследуются. Тем не менее, если текущий класс перекрывает все назначенные инициализаторы суперкласса, он наследует вспомогательные инициализаторы суперкласса. Классы в Swift не наследуют от универсального базового класса.

Есть два способа создания экземпляра ранее заявленного класса:

Вызов один из инициализаторов, объявленных внутри класса, как описано в Инициализаторы.

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

Получите доступ к свойствам экземпляра класса через синтаксис: точка (.), как описано в Доступ к свойствам.

Классы являются ссылочными типами. Экземпляры класса на них ссылаются, а не копируются, когда присвоены переменным или константам, или при передаче в качестве аргументов вызова функции. Подробнее см. Структуры и перечисления - типы значения.

Вы можете расширить поведение типа класса через объявление расширения, см. Объявление расширения.

Грамматика объявления класса

class-declaration → attributesopt  access-level-modifieropt  finalopt  class class-name  generic-parameter-clauseopt  type-inheritance-clauseopt  generic-where-clauseopt  class-body class-declaration → attributesopt  final access-level-modifieropt  class class-name  generic-parameter-clauseopt  type-inheritance-clauseopt  generic-where-clauseopt  class-body class-name → identifier class-body → { class-membersopt } class-members → class-member  class-membersopt class-member → declaration  |  compiler-control-statement

Объявление протокола

Объявление протокола вводит именованный тип протокола в вашу программу. Объявления протокола объявляются в глобальной области видимости с помощью ключевого слова protocol и выглядят следующим образом:

protocol имя протокола: наследуемые протоколы {

объявление членов протокола

}

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

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

Заметка

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

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

По умолчанию, типы, соответствующие протоколу, должны реализовывать все свойства, методы и сабскрипты, объявленные в протоколе. Тем не менее, вы можете отметить эти объявления членов протокола модификатором объявления optional, чтобы указать, что их реализация по типу соответствия требованиям является опциональной. Модификатор optional может быть применен только к членам, отмеченным атрибутом objc и только к членам протоколов, отмеченных атрибутом objc. В результате, только классовые типы могут принимать и соответствовать протоколу, который содержит требования опциональных членов. Для получения дополнительных сведений о том, как использовать модификатор объявления optional и получить доступ к опциональным членам протокола, например, когда вы не уверены, реализует ли их соответствующий требованиям тип см. Опциональные требования протокола.

Для того, чтобы ограничить принятие протокола только до классовых типов, отметьте протокол с требованием class, написав ключевое слово class в качестве первого элемента в списке наследуемых протоколов после двоеточия. Например, следующий протокол может быть принят только по классовым типам:

protocol SomeProtocol: AnyObject {

    /* Члены протокола */

}

Любой протокол, который наследует от протокола, отмеченного требованием class, также может быть принят только по классовым типам.

Заметка

Если протокол отмечен атрибутом objc, требование class неявно применяется к этому протоколу. Нет никакой необходимости отмечать протокол требованием class явно.

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

Вы можете использовать протоколы для объявления того, какие методы делегат класса или структуры должен реализовывать, как описано в Делегирование.

Грамматика объявления протокола

protocol-declaration → attributesopt access-level-modifieropt protocol protocol-name type-inheritance-clauseopt protocol-body protocol-name → identifier protocol-body → { protocol-member-declarationsopt } protocol-member-declaration → protocol-property-declaration protocol-member-declaration → protocol-method-declaration protocol-member-declaration → protocol-initializer-declaration protocol-member-declaration → protocol-subscript-declaration protocol-member-declaration → protocol-associated-type-declaration protocol-member-declarations → protocol-member-declaration protocol-member-declarationsopt

Объявление свойств протокола

Протоколы объявляют, что соответствующие типы должны реализовывать свойство, включая объявление свойства протокола в тело объявления протокола. Объявления свойств протокола имеют специальную форму объявления переменной:

var имя свойства: тип { get set }

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

Требования геттера и сеттера могут быть удовлетворены соответствующим типом различными способами. Если объявление свойства включает ключевые слова get и set, то соответствующий тип может реализовать его через переменную свойств хранения или вычисляемое свойство, одновременно удобные для чтения и записи (то есть то, которое реализует как геттер, так и сеттер). Однако объявление свойства не может быть реализовано в виде свойства константы или вычисляемого свойства,  доступного только для чтения. Если объявление свойства включает в себя только ключевое слово get, оно может быть реализовано как любой вид свойства. Подробнее Требование свойств.

См. также Объявление переменных.

Грамматика объявления свойств протокола

protocol-property-declaration → variable-declaration-head  variable-name  type-annotation  getter-setter-keyword-block

Объявление методов протокола

Протоколы объявляют, что соответствующие типы должны реализовывать метод, включая объявление метода протокола в тело объявления протокола. Объявления методов протокола имеют тот же вид, что и объявления функций, с двумя исключениями: они не включают в себя тело функции, и вы не можете предоставить какие-либо дефолтные значения параметров как часть объявления функции. Примеры соответствующих типов, реализующих требования метода протокола, можно найти в Требования методов.

Для объявления класса или требования статического метода в объявлении протокола, отметьте объявление метода модификатором объявления static. Классы, реализующие этот метод, объявляют метод модификатором class. Структуры, реализующие его, должны вместо этого объявлять метод модификатором объявления static. Если вы реализуете метод в расширении, используйте модификатор class, если вы расширяете класс, и модификатор static, если вы расширяете структуру.

Cм. также Объявление функции.

Грамматика объявления метода протокола

protocol-method-declaration → function-head  function-name  generic-parameter-clauseopt   function-signature

Объявление инициализатора протокола

Протоколы объявляют, что соответствующие им типы должны реализовывать инициализатор, включая объявление инициализатора протокола в тело объявления протокола. Объявления инициализатора протокола имеют ту же форму, что и объявления инициализатора, за исключением того, что они не включают в себя тело инициализатора.

Соответствующий тип может удовлетворить требование непроваливающегося инициализатора протокола через реализацию непроваливающегося инициализатора или проваливающегося инициализатора init!. Соответствующий тип может удовлетворить требование проваливающегося инициализатора протокола через реализацию инициализатора любого вида.

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

Cм. также Объявление инициализатора.

Грамматика объявления инициализатора протокола

protocol-initializer-declaration → initializer-head  generic-parameter-clauseopt  parameter-clause throwsopt protocol-initializer-declaration → initializer-head  generic-parameter-clauseopt  parameter-clause rethrows

Объявление сабскрипта протокола

Протоколы объявляют, что соответствующие типы должны реализовывать сабскрипт, включая объявление сабскрипта протокола в тело объявления протокола. У объявления сабскрипта протокола есть специальная форма:

subscript (парметры) -> возвращаемый тип { get set }

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

Cм. также Объявления сабскрита.

Грамматика объявления сабскрипта протокола

protocol-subscript-declaration → subscript-head subscript-result getter-setter-keyword-block

Объявление ассоциированного типа протокола

Протоколы объявляют ассоциированные типы, используя ключевое слово associatedtype. Ассоциированный тип обеспечивает псевдоним для типа, который используется в качестве части объявления протокола. Ассоциированные типы аналогичны параметрам типа в общем положении параметров, но они связаны с Self в протоколе, в котором они объявлены. В этом контексте, Self относится к возможному типу, соответствующему протоколу. Подробнее Связанные типы.

Вы можете использовать where в объявлении протокола для добавления ограничений связанным типам, унаследованным от другого протокола, без повторного использования связанных типов. Например, объявления SubProtocol ниже эквивалентны:

protocol SomeProtocol {

associatedtype SomeType

}

protocol SubProtocolA: SomeProtocol {

// This syntax produces a warning.

associatedtype SomeType: Equatable

}

// This syntax is preferred.

protocol SubProtocolB: SomeProtocol where SomeType: Equatable { }

Грамматика объявления ассоциированного типа

protocol-associated-type-declaration → attributesopt access-level-modifieropt associatedtype typealias-name type-inheritance-clauseopt typealias-assignmentopt

Объявление инициализатора

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

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

Следующая форма объявляет инициализаторы для структур, перечислений и назначенных инициализаторов классов:

init(параметры) {

инструкции

}

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

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

Инициализаторы в структурах и перечислениях могут вызывать другие объявленные инициализаторы для части делегирования или всего процесса инициализации.

Для того, чтобы объявить вспомогательные инициализаторы для класса, отметьте объявление инициализатора модификатором объявления convenience.

convenience init(параметры) {

инструкции

}

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

Вы можете отметить назначенные и вспомогательные инициализаторы необходимым модификатором объявления required, чтобы каждый подкласс реализовывал инициализатор. Реализация подкласса этого инициализатора также должна быть отмечена модификатором объявления required.

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

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

Заметка

Если вы отмечаете инициализатор модификатором объявления required, вам не нужно отмечать инициализатор модификатором override, когда вы переопределяете требуемый инициализатор в подклассе.

Так же, как функции и методы, инициализаторы могут выбрасывать или повторно генерировать ошибки. И точно так же, как функции и методы, вы используете ключевые слова throws или rethrows после того, как параметры инициализатора, чтобы указать соответствующее поведение.

Чтобы увидеть примеры инициализатора в различных объявлений типа см. Инициализация.

Проваливающиеся инициализаторы

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

Чтобы объявить проваливающийся инициализатор, производящий опциональный экземпляр, добавьте знак вопроса к ключевому слову init в объявлении инициализатора (init?). Чтобы объявить проваливающийся инициализатор, производящий неявно извлеченный опциональный экземпляр, добавьте вместо восклицательный знак (init!). В приведенном ниже примере показан проваливающийся инициализатор init?, производящий опциональный экземпляр структуры.

struct SomeStruct {

let property: String

// создает опциональный экземпляр структуры'SomeStruct'

init?(input: String) {

if input.isEmpty {

// избавляемся от 'self' и возвращаем 'nil'

return nil

}

property = input

}

}

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

if let actualInstance = SomeStruct(input: "Hello") {

// что-то делаем с экземпляром структуры 'SomeStruct'

} else {

// инциализация структуры 'SomeStruct' провалилась и инициализатор вернул 'nil'

}

Проваливающийсяинициализатор может возвращать nil в любой момент реализации тела инициализатора.

Проваливающийся инициализатор может делегировать любым видам инициализаторов. Непроваливающийся инициализатор может делегировать другому непроваливающемуся инициализатору или проваливающемуся инициализатору init!. Непроваливающийся инициализатор может делегировать проваливающемуся инициализатору init? через принудительное извлечение результата инициализатора, например через написание super.init()!.

Неудачная инициализация распространяется через делегирование инициализатора. В частности, если проваливающийся инициализатор делегирует инициализатору, который падает и возвращает nil, то инициализатор, который делегировал, также падает и неявно возвращает nil. Если непроваливающийся инициализатор делегирует проваливающемуся инициализатору init!, который падает и возвращает nil, то возникает ошибка выполнения (как если бы вы использовали оператор ! для извлечения опционала со значением nil).

Проваливающийся назначенный инициализатор может быть переопределен в подклассе любым видом назначенного инициализатора. Непроваливающийся назначенный инициализатор может быть переопределен в подклассе только непроваливающимся назначенным инициализатором.

Подробнее см. Проваливающиеся инициализаторы.

Грамматика объявления инициализатора

initializer-declaration → initializer-head generic-parameter-clauseopt parameter-clause throwsopt initializer-body initializer-declaration → initializer-head generic-parameter-clauseopt parameter-clause rethrows initializer-body initializer-head → attributesopt declaration-modifiersopt init initializer-head → attributesopt declaration-modifiersopt init? initializer-head → attributesopt declaration-modifiersopt init! initializer-body → code-block

Объявление деинициализатора

Объявление деинициализатора объявляет деинициализатор для типа класса. Деинициализаторы не принимают никаких параметров и имеют следующий вид:

deinit {

инструкции

}

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

Подкласс наследует деинициализатор суперкласса, который неявно вызывается непосредственно перед тем, как объект класса освобождается. Объект подкласса не освобождается, пока все деинициализаторы в цепочке наследования не закончат выполнение.

Деинициализаторы не вызываются напрямую.

Подробнее Деинициализация.

Грамматика объявления деинициализатора

deinitializer-declaration → attributesopt deinit code-block

Объявление расширения

Объявление расширения позволяет расширять поведение существующих типов класса, структур и перечислений. Объявления расширений объявляются с помощью ключевого слова extension и имеют следующий вид:

extension имя типа: принятые протоколы {

объявления

}

Тело объявления расширения содержит ноль или более объявлений. Эти объявления могут включать вычисляемые свойства, вычисляемые свойства типа, методы, например, методы типа, инициализаторы, объявления сабскриптов, и даже классы, структуры и объявления перечислений. Объявления расширений не могут содержать деинициализаторы или объявления протокола, свойства хранения, наблюдатели свойств или другие объявления расширения. Подробнее Расширения.

Если имя типа является классом, структурой или типом перечисления, расширение расширяет этот тип. Если имя типа является типом протокола, расширение расширяет все типы, соответствующие этому протоколу.

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

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

Свойства, методы и инициализаторы существующего типа не могут быть переопределены в расширении этого типа.

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

extension type name: adopted protocols where requirements {

declarations

}

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

Условное соответствие

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

Переопределенные требований не используемых в некоторых универсальных шаблонах

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

protocol Loggable {

func log()

}

extension Loggable {

func log() {

print(self)

}

}

protocol TitledLoggable: Loggable {

static var logTitle: String { get }

}

extension TitledLoggable {

func log() {

print("\(Self.logTitle): \(self)")

}

}

struct Pair: CustomStringConvertible {

let first: T

let second: T

var description: String {

return "(\(first), \(second))"

}

}

extension Pair: Loggable where T: Loggable { }

extension Pair: TitledLoggable where T: TitledLoggable {

static var logTitle: String {

return "Pair of '\(T.logTitle)'"

}

}

extension String: TitledLoggable {

static var logTitle: String {

return "String"

}

}

Структура Pair соответствует Loggable и TitledLoggable всякий раз, когда ее общий тип соответствует Loggable или TitledLoggable, соответственно. В приведенном ниже примере oneAndTwo является экземпляром Pair , который соответствует TitledLoggable, потому что String соответствует TitledLoggable. Когда метод log() вызывается непосредственно в oneAndTwo, используется специализированная версия, содержащая строку заголовка.

let oneAndTwo = Pair(first: "one", second: "two")

oneAndTwo.log()

// Prints "Pair of 'String': (one, two)"

Однако, когда oneAndTwo используется в общем контексте или в качестве экземпляра протокола Loggable, специализированная версия не используется. Swift выбирает, какую реализацию log() вызывать, консультируясь только с минимальными требованиями - Pair должен соответствовать Loggable. По этой причине, вместо этого используется реализация по умолчанию, предоставляемая протоколом Loggable.

func doSomething<T: Loggable>(with x: T) {

x.log()

}

doSomething(with: oneAndTwo)

// Выведет "(one, two)"

Когда log() вызывается в экземпляре, который передан doSomething (_ , настраиваемый заголовок не указывается в занесенной строке.

Соответствие протокола не должно быть избыточным

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

Разрешение явной избыточности

Несколько расширений на конкретном типе не могут добавить соответствие одному и тому же протоколу, даже если требования расширений являются взаимоисключающими. Это ограничение показано в приведенном ниже примере. Два объявления расширений пытаются добавить условное соответствие протоколу Serializable, один для массивов с элементами Int и один для массивов со строковыми элементами.

protocol Serializable {

func serialize() -> Any

}

extension Array: Serializable where Element == Int {

func serialize() -> Any {

// Реализация

}

}

extension Array: Serializable where Element == String {

func serialize() -> Any {

// Реализация

}

}

// Ошибка: избыточное соответствие 'Array<Element>' протоколу 'Serializable'

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

protocol SerializableInArray { }

extension Int: SerializableInArray { }

extension String: SerializableInArray { }

extension Array: Serializable where Element: SerializableInArray {

func serialize() -> Any {

// Реализация

}

}

Разрешение неявной избыточности

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

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

В следующем примере явно объявляется условное соответствие массива Loggable, чтобы избежать конфликта при объявлении его условного соответствия как TitledLoggable, так и нового протокола MarkedLoggable.

protocol MarkedLoggable: Loggable {

func markAndLog()

}

extension MarkedLoggable {

func markAndLog() {

print("----------")

log()

}

}

extension Array: Loggable where Element: Loggable { }

extension Array: TitledLoggable where Element: TitledLoggable {

static var logTitle: String {

return "Array of '\(Element.logTitle)'"

}

}

extension Array: MarkedLoggable where Element: MarkedLoggable { }

Чтобы явно объявить условное соответствие Loggable без расширения, другие расширения Array будут неявно создавать эти объявления, что приведет к ошибке:

extension Array: Loggable where Element: TitledLoggable { }

extension Array: Loggable where Element: MarkedLoggable { }

// Ошибка: избыточное соответствие 'Array<Element>' протоколу 'Loggable'

Грамматика объявления расширений

extension-declaration → attributesopt  access-level-modifieropt  extension type-identifier  type-inheritance-clauseopt  extension-body extension-declaration → attributesopt  access-level-modifieropt  extension type-identifier  generic-where-clause  extension-body extension-body → { extension-membersopt } extension-members → extension-member  extension-membersopt extension-member → declaration  compiler-control-statement

Объявления сабскрита

Объявления сабскрипта позволяет вам добавлять поддержку сабскрипта для объектов определенного типа и, как правило, используются для обеспечения удобным синтаксисом доступа к элементам в коллекции, списке или последовательности. Сабскрипты объявляются с помощью ключевого слова subscript и имеют следующий вид:

subscript (парметры) -> возвращаемый тип {

get {

инструкции

}

set(имя сеттера) {

инструкции

}

}

Объявления сабскрипта могут появляться только в контексте класса, структуры, перечисления, расширения или объявления протокола.

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

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

Имя сеттера и полукруглые скобки являются опциональными. Если вы предоставляете имя сеттера, то оно используется в качестве имени параметра сеттера. Если вы не предоставляете имя сеттера, то дефолтное имя параметра сеттера будет value. Тип имени сеттера должен быть таким же, как тип возвращаемого значения.

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

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

Вы также можете объявить сабскрипты в контексте объявления протокола.

Для получения дополнительной информации о сабскриптах см. Индексы.

Грамматика объявления сабскрипта

subscript-declaration → subscript-head subscript-result code-block

subscript-declaration → subscript-head subscript-result getter-setter-block

subscript-declaration → subscript-head subscript-resultgetter-setter-keyword-block

subscript-head → attributesopt declaration-modifiersopt subscript parameter-clause subscript-result → ->attributesopt type

Объявление оператора

Объявление оператора вводит новый инфиксный, префиксный или постфиксный оператор в вашу программу и объявляется с помощью ключевого слова operator.

Вы можете объявить операторов трех различных вариантов: инфиксных, префиксных и постфиксных. Вариант положения оператора определяется положением оператора по отношению к своим операндам.

Существуют три основные формы объявления оператора, по одной для каждого варианта положения. Вариант положения оператора определяется через отметку объявления оператора модификатором объявления infix, prefix, или postfix перед ключевым словом operator. В каждой форме название оператора может содержать только символы оператора, определенные в Операторы.

Следующая форма объявляет новый инфиксный оператор:

infix operator имя оператора : группа приоритета

Инфиксный оператор - это бинарный оператор, который записывается между двумя операндами, например распространенное дополнение оператора (+) в выражении 1 + 2.

Инфиксные операторы могут дополнительно указывать группу приоритета. Если вы пропускаете указание группы приоритета при создании оператора, то Swift использует дефектную группу - DefaultPrecedence, которая определена более высоким приоритетом, чем TernaryPrecedence. Для более подробной информации, см. Объявление групп приоритета.

Следующая форма объявляет новый префиксный оператор:

prefix operator имя оператора {}

Префиксный оператор - это унарный оператор, который записывается непосредственно перед его операндом, например, префикс логического оператора NOT (!) В выражении !a.

Объявление префиксных операторов не указывает уровень приоритета. Префиксные операторы неассоциативны.

Следующая форма объявляет новый постфиксный оператор:

postfix operator имя оператора {}

Постфиксный оператор - это унарный оператор, который записывается сразу после своего операнда, например, постфиксный оператор принудительного развоваричавания (!) в выражении a!.

Как и в случае с префиксными оператораторами, объявление постфиксных операторов не определяет уровень приоритета. Постфиксные операторы являются неассоциативными.

После объявления нового оператора, вы реализуете его, объявляя функцию, имеющую то же имя, что и оператор. Если вы реализуете префиксный или постфиксный оператор, то вам необходимо также отметить это объявление функции модификатором объявления prefix или postfix. Если вы реализуете инфиксный оператор, то вы не отмечаете объявление функции модификатором объявления infix. Подробнее Пользовательские операторы.

Грамматика объявления оператора

operator-declaration → prefix-operator-declaration | postfix-operator-declaration | infix-operator-declaration prefix-operator-declaration → prefix operator operator postfix-operator-declaration → postfix operator operator infix-operator-declaration → infix operator operator infix-operator-attributesopt infix-operator-group → : precedence-group-name

Объявление групп приоритета

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

Объявления групп приоритета имеет следующую форму:

precedencegroup имя группы приоритета {

higherThan: имя нижней группы

lowerThan: имя верхней группы

associativity: ассоциативность

assignment: присваивание

}

Списки имен нижней и верхней групп определяют отношение новой группы приоритета к уже существующим группам. Атрибут группы приоритета lowerThan может быть использован только для ссылки на другие группы приоритета, которые объявлены за пределами текущего модуля. Когда два оператора соревнуются между собой за свои операнды, как например, в случае 2 + 3 * 5, побеждает оператор, который более тесно связан со своими операндами.

Заметка

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

Swift определяет множество групп приоритета для использования вместе с операторами определенных в стандартной библиотеке Swift. Например операторы сложения (+) и вычитания (-) принадлежат к группе AdditionPrecedence, а операторы умножения (*) и деления (/) принадлежат группе MultiplicationPrecedence. Для полного списка групп приоритета предоставляемых стандартной библиотекой Swift смотрите Operator Declarations.

Ассоциативность оператора определяет как последовательность операторов с одним и тем же уровнем приоритета группируются вместе в случае отсутствия группирующих круглых скобок. Ассоциативность определяется одним из регистрозависимых ключевых слов left, right, none. Если вы пропускаете ассоциативность, то она по умолчанию устанавливается как none. Операторы, которые имеют левую ассоциативность работают слева на право. Например, оператор вычитания (-) является левоассоциативным, так что выражение 4 - 5 - 6 группируется как (4 - 5) - 6 и вычисляется как -7. Операторы, которые имеют правую ассоциативность, работают справа на лево. Операторы, которые имеют ассоциативность none, не ассоциируются. Операторы с ассоциативностью none не могут располагаться рядом друг с другом. Например, оператор < имеет ассоциативность none, что значит 1 < 2 < 3 не является корректным выражением.

Assignment в группе приоритета определяет приоритет оператора, когда он используется в операции, где имеет место быть опциональная цепочка. Когда assignment имеет значение true, то оператор соответствующей группы приоритета использует те же правила внутри опциональной цепочки, что и операторы присваивания из стандартной библиотеки. В противном случае, когда assignment имеет значение false, операторы из группы приоритета следуют тем же правилам опциональной цепочки, что и операторы не выполняющие присваивания.

Грамматика объявления групп приоритета

recedence-group-declaration → precedencegroup precedence-group-name { precedence-group-attributes opt } precedence-group-attributes → precedence-group-attribute precedence-group-attributes opt precedence-group-attribute → precedence-group-relation precedence-group-attribute → precedence-group-assignment precedence-group-attribute → precedence-group-associativity precedence-group-relation → higherThan : precedence-group-names precedence-group-relation → lowerThan : precedence-group-names precedence-group-assignment → assignment : boolean-literal precedence-group-associativity → associativity : left precedence-group-associativity → associativity : right precedence-group-associativity → associativity : none precedence-group-names → precedence-group-name  |  precedence-group-name,precedence-group-names precedence-group-name → identifier

Модификаторы объявления

Модификаторы объявления - это ключевые слова или контекстно-зависимые ключевые слова, которые изменяют поведение или значение объявления. Вы указываете модификатор объявления через написание соответствующего ключевого слова или контекстно-зависимого ключевого слова между атрибутами объявления (если таковые имеются) и ключевым словом, которое вводит объявление.

dynamic

Примените этот модификатор к любому члену класса, который может быть представлен Objective-C. Когда вы отмечаете объявление члена модификатором dynamic, доступ к этому члену всегда динамически отправляется во время выполнения Objective-C. Доступ к этому члену никогда не встраивается или девиртуализуется компилятором.

Поскольку объявления, отмеченные модификатором dynamic, отправляются во время выполнения Objective-C, они неявно отмечаются атрибутом objc.

final

Примените этот модификатор к классу или свойству, методу или члену сабскрипта класса. Он применяется к классу, чтобы указать, что класс не может иметь подклассы. Он применяется к свойству, методу или сабскрипту класса, чтобы указать, что член класса не может быть переопределен в любом подклассе. Для того, чтобы рассмотреть пример использования атрибута final, см. Предотвращение переопределений.

lazy

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

optional

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

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

required

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

unowned

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

unowned(safe)

Явное указание unowned

unowned(unsafe)

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

weak

Модификатор weak применяется к переменной или переменной свойства хранения, чтобы указать, что переменная или свойство имеет слабую ссылку на объект, хранящийся в качестве его значения. Тип переменной или свойства должен быть опциональным типом класса. Используйте модификатор weak, чтобы избежать сильных ссылочных циклов. Подробнее Weak ссылки.

Уровни контроля доступа

Swift обеспечивает три уровня доступа: public, internal и private. Вы можете отметить объявление одним из модификаторов доступа, описанные ниже, чтобы указать уровень доступа объявления. Контроль доступа подробно обсуждается в Контроль доступа.

open

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

public

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

internal

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

fileprivate

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

private

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

Каждый модификатор уровня доступа, описанный выше, опционально принимает один аргумент, который состоит из ключевого слова set в круглых скобках (например, private(set)). Используйте эту форму модификатора уровня доступа, если вы хотите указать уровень доступа для сеттера переменной или сабскрипта, что меньше или равно уровню доступа переменной или самого сабскрипта, что подробно описано в Геттеры и сеттеры.

Грамматика объявления модификатора

declaration-modifier → class | convenience | dynamic | final | infix | lazy | optional | override | postfix | prefix | required | static | unowned | unowned(safe) | unowned(unsafe) | weak declaration-modifier → access-level-modifier declaration-modifier → mutation-modifier declaration-modifiers → declaration-modifierdeclaration-modifiersopt access-level-modifier → private  | private(set) access-level-modifier → fileprivate | fileprivate(set) access-level-modifier → internal | internal(set) access-level-modifier → public | public(set) access-level-modifier → open | open(set) mutation-modifier → mutating | nonmutating

Атрибуты

Атрибуты предоставляют больше информации о объявлении или типе. Всего два типа атрибутов в Swift, которые применимы к объявлениям и которые применимы к типам.

Вы обозначаете атрибут при помощи @, за которым следует имя атрибута и аргументы, которые принимает данный атрибут:

@имя атрибута

@имя атрибута ( аргументы атрибута )

Некоторые атрибуты объявления принимают аргументы, которые указывают больше информации об атрибуте и как он относится к конкретному объявлению. Эти аргументы атрибутов помещены в круглые скобки, а их формат определен атрибутом, к которому они принадлежат.

Атрибуты объявления

Вы можете применить атрибут объявления только к объявлениям. Однако вы так же можете применить атрибут noreturn к функции или к методу типа.

available

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

Атрибут available всегда появляется с двумя или более разделенных между собой аргументами. Эти аргументы начинаются с одного из имен названий платформ:

iOS

iOSApplicationExtension

macOS

OSXApplicationExtension

watchOS

watchOSApplicationExtension

tvOS

tvOSApplicationExtension

swift

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

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

Аргумент unavailable является индикатором того, что объявление не доступно для указанной платформы .

Аргумент introduced указывает первую версию указанной платформы, в которой впервые было введено объявление.

introduced: номер версии

Число version number состоит из одного или более положительных целых чисел, разделенных между собой точками.

Аргумент deprecated указывает версию определенной платформы, начиная с которой указанное объявление будет исключено. Он имеет следующую форму:

deprecated: номер версии

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

Аргумент obsoleted указывает на первую версию платформы, начиная с которого данное объявление считается устаревшим. Когда объявление устаревает, оно удаляется с указанной платформы и не может быть больше использовано. Аргумент имеет следующую форму записи:

obsoleted: номер версии

Число version number состоит из одного или более положительных целых чисел, разделенных между собой точками.

Аргумент message используется для предоставления текстовой информации, которая отображается компилятором, когда появляется предупреждение или ошибка об использовании устаревшего или уже исключенного объявления. Этот аргумент имеет следующую форму:

message: сообщение

Аргумент message состоит из строкового литерала.

Аргумент renamed используется для предоставления текстового сообщения, которое показывает новое имя объявления, которое было изменено. Новое имя отображается компилятором, когда появляется о шибка об использовании переименованного объявления. Этот атрибут имеет следующую форму:

renamed: новое имя

Аргумент новое имя состоит из строкового литерала.

Вы можете использовать аргумент renamed в сочетании с аргументом unavailable и в объявлении псевдонима типа, для отображения того, что старое объявление было удалено. Например, это бывает полезно когда имя объявления поменялось между различными релизами фреймворка или библиотеки.

    // Первый релиз

protocol MyProtocol {

    // определение протокола

}

// Переименование MyProtocol в последующих релизах

protocol MyRenamedProtocol {

    // определение протокола

}

@available(*, unavailable, renamed="MyRenamedProtocol")

typealias MyProtocol = MyRenamedProtocol

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

Если атрибут available только указывает аргумент introduced в дополнение к аргументу имени платформы, то может быть использован следующий краткий синтаксис:

@available(имя платформы номер версии, *)

Краткий синтаксис для атрибутов available позволяет выразить доступность для нескольких платформ в краткой форме. Несмотря на то, что эти обе формы эквивалентны, но предпочтительнее использовать краткую форму.

@available(iOS 10.0, macOS 10.12, *)

class MyClass {

    // определение класса

}

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

@available(swift 3.0.2)

@available(macOS 10.12, *)

struct MyStruct {

// struct definition

}

discardableResult

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

dynamicMemberLookup

Примените этот атрибут к классу, структуре, перечислению или протоколу, чтобы осуществить поиск по имени во время выполнения. Тип должен реализовать подстрочный индекс subscript(dynamicMemberLookup:).

В явном выражении члена, если нет соответствующего объявления для именования, выражение понимается как вызов сабскрипта типа subscript(dynamicMemberLookup:), передавая строковый литерал, содержащий имя члена в качестве аргумента. Тип параметра сабскрипта может быть любым типом, который соответствует протоколу ExpressibleByStringLiteral, его возвращающее значение может быть любого типа. В большинстве случаев параметр сабскрипта является значением String. Например:

@dynamicMemberLookup

struct DynamicStruct {

let dictionary = ["someDynamicMember": 325,

"someOtherMember": 787]

subscript(dynamicMember member: String) -> Int {

return dictionary[member] ?? 1054

}

}

let s = DynamicStruct()

// Using dynamic member lookup

let dynamic = s.someDynamicMember

print(dynamic)

// Выведет "325"

// Calling the underlying subscript directly

let equivalent = s[dynamicMember: "someDynamicMember"]

print(dynamic == equivalent)

// Выведет "true"

GKInspectable

Примените этот атрибут, чтобы открыть кастомное свойство компонента GameplayKit в интерфейсе редактора SpriteKit. Применение этого атрибута также подразумевает атрибут objc.

inlinable

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

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

nonobjc

Применяйте этот атрибут к методу, свойству, сабскрипту или к объявлению инициализатора для неявного запрета использования атрибута objc Атрибут nonobjc говорит компилятору сделать текущее объявление невозможным в Objective-C, даже если это возможно.

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

Методы имеющие маркировку nonobjc не могут переопределять методы с атрибутом objc. Однако метод с атрибутом objc может переопределять методы с атрибутом nonobjc. Аналогично методы имеющие атрибут nonobjc не могут удовлетворить требования протокола с атрибутом @objc.

NSApplicationMain

Применяйте этот атрибут к классу для индикации того, что он является делегатом приложения.

Применение этого атрибута эквивалентно вызову функции NSApplicationMain(_:_:) и передачей в нее имени класса в качестве имени делегируемого класса.

Если вы не используете этот атрибут, то разместите в main.swift функцию main(), которая вызывает NSApplicationMain(_:_:).

import AppKit

NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

NSCopying

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

Атрибут NSCopying ведет себя аналогично атрибуту свойства copy в Objective-C.

NSManaged

Применяйте этот атрибут к методу или переменному свойству хранения экземпляра класса, который наследует от NSManagedObject для указания того, что Core Data динамически предоставляет его реализацию во время исполнения, основываясь на ассоциативном описании сущности. Для свойства с маркировкой NSManaged, Core Data так же обеспечивает хранение во время исполнения.

objc

Применяйте этот атрибут в любом объявлении, которое может быть отображено в Objective-C, например, в невложенных классах, протоколах, не универсальных перечислениях (ограниченными целочисленным типом), свойства и методы (включая геттеры и сеттеры) классов и протоколов, инициализаторы, деинициализаторы и сабскрипты. Атрибут objc говорит компилятору, что объявление доступно для использования в Objective-C коде.

Компилятор так же неявно добавляет атрибут objc к классу, который наследует от другого класса с атрибутом objc или от класса определенного в Objective-C. Однако класс не должен быть универсальным типом и не должен наследоваться от каких-либо классов универсального типа. Если вы приписываете атрибут objc к классу или протоколу, то он неявно применяется к Objective-C соответствующим членам этого класса или протокола. Протоколы с маркировкой атрибута objc не могут наследовать от протоколов без этой маркировки.

Атрибут objc также неявно добавляется в следующих случаях:

Объявление является переопределением в подклассе, а объявление суперкласса имеет атрибут objc.

Объявление удовлетворяет требованию протокола, который имеет атрибут objc.

Объявление имеет атрибуты IBAction, IBOutlet, IBDesignable, IBInspectable, NSManaged и GKInspectable.

Если вы примените атрибут objc к перечислению, то каждый кейс перечисления подвергается воздействию кода Objective-C как объединение имени перечисления и имени кейса. Например, кейс с именем venus в перечислении Swift Planet под воздействием Objective-C воспринимается как кейс с именем PlanetVenus.

Опционально атрибут objc принимает единственный аргумент, который состоит из идентификатора. Используйте этот атрибут тогда, когда вы хотите указать другое имя в Objective-C для того, к чему применяется objc атрибут. Вы можете использовать этот атрибут в именах классов, перечислений, кейсах перечислений, протоколах, методах, сеттерах, геттерах и инициализаторах. Если вы укажете имя Objective-C для класса, протокола или перечисления, укажите трехбуквенный префикс на имя, как описано в Соглашении в Программировании Objective-C. Пример ниже изменяет геттер для свойства enabled класса ExampleClass в Objective-C коде на isEnabled.

@objc

class ExampleClass: NSObject {

    var enabled: Bool {

        @objc(isEnabled) get {

            // Return the appropriate value

        }

    }

}

objcMembers

Примените этот атрибут к любому объявлению класса, которое может иметь атрибут objc. Атрибут objc неявно добавляется к совместимым с Objective-C членам класса, его расширениям, его подклассам и всем расширениям его подклассов.

В большинстве случаев код должен использовать атрибут objc, чтобы выставлять только те объявления, которые необходимы. Если вам нужно выставить много объявлений, вы можете сгруппировать их в расширение, имеющее атрибут objc. Атрибут objcMembers является удобством для библиотек, которые интенсивно используют средства самоанализа в среде выполнения Objective-C. Применение атрибута objc, когда оно не требуется, может увеличить ваш двоичный размер и отрицательно повлиять на производительность.

testable

Применяйте этот атрибут для импорта объявлений для компилируемых модулей с включенным режимом тестинга для доступа к сущностям с уровнем доступа internal как будто они имеют уровень доступа public. Тесты также могут обращаться к классам и членам класса, которые помечены внутренним или общедоступным модификатором уровня доступа, как если бы они были объявлены с помощью модификатора открытого уровня доступа.

UIApplicationMain

Применяйте этот атрибут к классу для отображения того, что этот класс является “делегатом приложения”. Использование этого атрибута эквивалентно вызову функции UIApplicationMain и передачи в нее имени класса, в качестве имени класса-делегата.

Если вы не используете этот атрибут, то вы должны предоставить в main.swift в функцию main вызов функции UIApplicationMain(_:_:_:). Например, если ваше приложение использует пользовательский подкласс UIApplication в качестве своего основного класса, то вызывайте функцию UIApplicationMain(_:_:_:) вместо использования этого атрибута.

usableFromInline

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

Подобно модификатору уровня доступа Public, этот атрибут предоставляет объявление как часть открытого интерфейса модуля. В отличие от public, компилятор не разрешает объявления, помеченные с помощью usableFromInline, ссылаться по имени в коде вне модуля, даже несмотря на то, что объявление экспортируется. Тем не менее, код вне модуля может все еще иметь возможность взаимодействовать с объявлением, используя режим во время выполнения.

Объявления, отмеченные встроенным атрибутом, неявно используются из встроенного кода. Несмотря на то, что для внутренних объявлений может применяться как встроенная, так и полезная функция FromInline. Применение обоих атрибутов является ошибкой.

warn_unqualified_access

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

Например, стандартная библиотека Swift включает в себя две функции высшего порядка: min(_:_:) и min() для последовательностей со сравниваемыми элементами. Этот метод последовательности объявлен с атрибутом warn_unqualified_access для уменьшения путаницы, которая может возникнуть при использовании одного или другого метода внутри расширения Sequence.

Объявление атрибутов с использованием Interface Builder

Атрибуты интерфейс билдер являются атрибутами объявления и используются интерфейс билдером для синхронизации с Xcode. Swift предоставляет следующие атрибуты интерфейс билдера: IBAction, IBDesignable, IBInspectable, и IBOutlet. Эти атрибуты концептуально идентичны своим Objective-C коллегам.

Вы можете применить атрибуты IBOutlet и IBInspectable при объявлении свойства внутри класса. Вы применяете атрибут IBAction к объявлению метода внутри класса, а атрибут IBDesignable вы применяете к объявлению самого класса.

И IBAction, IBOutlet, IBDesignable и IBInspectable неявно подразумевают атрибут objc.

Атрибуты типа

Вы можете применять атрибуты типа только к самим типам. Однако, вы можете использовать атрибут noreturn к объявлению функции или метода.

autoclosure

Этот атрибут используется для того, чтобы отложить вычисление выражения, автоматически заворачивая выражение в замыкание без аргументов. Применяйте этот атрибут к объявлению параметра для функции или метода типа, которая не принимает аргументов и которая возвращает тип выражения. Объявления с атрибутом autoclosure так же подразумевают атрибут noescape, за исключением тех случаев, когда передан опциональный аргумент атрибута escaping. Пример использования autoclosure смотрите в “Автозамыкания" и "Функциональный тип".

convention

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

Атрибут convention всегда появляется с одним или более аргументами, которые указаны ниже.

Аргумент swift используется для отображения ссылки на функцию Swift. Это стандартное соглашение о вызове для функциональных типов в Swift.

Аргумент block используется для отображения ссылки на совместимый блок Objective-C. Значение функции представляется в виде ссылки на блок объекта, который является id-совместимым объектом Objective-C, который в свою очередь встраивает ее вызов функции внутрь объекта. Функция вызова использует соглашение о вызовах языка C.

Аргумент c используется для отображение ссылки на C функцию. Значение функции не несет никакого контекста и использует соглашение о вызовах языка C.

Функция с соглашением о вызовах языка C может быть использована в качестве функции с соглашением о вызовах Objective-C блоков, а функция с соглашением о вызовах. Objective-C блоков может быть использована с соглашением о вызовах функции Swift. Однако только неуниверсальные глобальные функции, локальные функции и замыкания, которые не захватывают значения локальных переменных, могут быть использованы как функции с соглашением о вызовах языка C.

escaping

Применяйте этот атрибут к объявлению функций или методов для индикации такого параметра, значение которого может быть сохранено для дальнейшего использования. Это значит, что значение может продолжать существовать после окончания вызова. Параметры функционального типа с атрибутом escaping требуют явного self. для свойств и методов. Пример использования escaping атрибутов можно посмотреть в главе Сбегающие замыкания.

Грамматика атрибута

attribute → @ attribute-nameattribute-argument-clauseopt attribute-name → identifier attribute-argument-clause → ( balanced-tokensopt ) attributes → attributeattributesopt balanced-tokens → balanced-tokenbalanced-tokensopt balanced-token → ( balanced-tokensopt ) balanced-token → [ balanced-tokensopt] balanced-token → { balanced-tokensopt } balanced-token → Any identifier, keyword, literal, or operator balanced-token → Any punctuation except (, ), [, ], {, or }

Шаблоны

Шаблон представляет собой структуру одного значения или составного значения. Например, структура кортежа (1, 2) представляет собой разделенный запятыми список, состоящий из двух элементов. Поскольку шаблоны представляют структуру значения, а не какое-либо конкретное значение, вы можете сопоставить их с различными значениями. Например, шаблон (x, y) совпадает с кортежем (1, 2), а также с любым другим кортежем, состоящем из двух элементов. В дополнение к тому, что шаблон совпадает с значением, вы также можете извлечь часть или всё составное значение и связать каждую его часть с именем константы или переменной.

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

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

Второй вид шаблона используется для полного совпадения с шаблоном, где значение, с которым вы пытаетесь совпасть, может не находится там во время выполнения. Сюда включены шаблоны кейсов перечисления, опциональные шаблоны, шаблоны выражений и шаблоны приведения типов. Вы можете использовать эти шаблоны в ярлыках кейсов инструкции switch, в условии catch инструкции do, или в условии инструкции if, while, guard, или for-in.

Грамматика шаблонов

pattern → wildcard-pattern type-annotationopt

pattern → identifier-pattern type-annotationopt

pattern → value-binding-pattern

pattern → tuple-pattern type-annotationopt

pattern → enum-case-pattern

pattern → optional-pattern

pattern → type-casting-pattern

pattern → expression-pattern

Шаблон wildcard

Шаблон wildcard совпадает и игнорирует любое значение и состоит из знака подчеркивания (_). Используйте этот шаблон, когда вам не важно имя, в которое присваивается значение. Например, следующий код перебирает в закрытом диапазоне 1...3, игнорируя текущее значение диапазона в каждой итерации цикла:

for _ in 1...3 {

// делаем что-то три раза

}

Грамматика шаблона wildcard

wildcard-pattern → _

Шаблон идентификатора

Шаблон идентификатора совпадает с любым значением и связывает совпавшее значение с именем переменной или константы. Например, в следующем объявлении константы, someValue является шаблоном идентификатора, который совпадает со значением 42 типа Int:

let someValue = 42

Когда совпадение проходит успешно, значение 42 связывается с именем константы someValue.

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

Грамматика шаблона идентификатора

identifier-pattern → identifier

Шаблон присваивания значения

Шаблон присваивания значения связывает соответствующие значения с именами переменных или констант. Шаблоны присваивания значения, связывают совпавшее значение с именем константы, которое начинается с ключевого слова let, а те, которые связываются с именем переменной начинается с ключевого слова var.

Шаблоны идентификатора в пределах шаблона со связанным значением связывают новые именованные переменные или константы с их совпавшими значениями. Например, вы можете разложить элементы кортежа и связать значение каждого элемента с соответствующим шаблоном идентификатора.

let point = (3, 2)

switch point {

// Связываем x и y с координатами точки

case let (x, y):

print("Точка имеет координаты (\(x), \(y)).")

}

// Выведет "Точка имеет координаты (3, 2)."

В приведенном выше примере let распространяется на каждый шаблон идентификатора в шаблоне кортежа (x, y). Из-за такого поведения, кейсы switch case let (x, y): и case (let x, let y): совпадают с одними и теме же значениями.

Грамматика шаблона присваивающего значение

value-binding-pattern → var pattern let pattern

Шаблон кортежа

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

Вы можете ограничить шаблон кортежа, чтобы он соответствовал определенным видам типов кортежа, используя аннотации типа. Например, шаблон кортежа (x, y): (Int, Int) в объявлении константы let (x, y): (Int, Int) = (1, 2) совпадает только с типами кортежа, в которых оба элемента типа Int.

Когда шаблон кортежа используется в качестве шаблона в инструкции for-in или в объявлении переменной или константы, он может содержать только шаблоны wildcard, шаблоны идентификаторов, опциональные шаблоны или другие шаблоны кортежей их содержащие. Например, следующий код не является рабочим, потому что элемент 0 в шаблоне кортежа (x, 0) является шаблоном выражения:

let points = [(0, 0), (1, 0), (1, 1), (2, 0), (2, 1)]

// Код не является рабочим

for (x, 0) in points {

/* ... */

}

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

let a = 2 // a: Int = 2

let (a) = 2 // a: Int = 2

let (a): Int = 2 // a: Int = 2

Грамматика шаблона кортежа

tuple-pattern → ( tuple-pattern-element-listopt )

tuple-pattern-element-list → tuple-pattern-element | tuple-pattern-element, tuple-pattern-element-list

tuple-pattern-element → pattern | identifier : pattern

Шаблон перечисления кейса

Шаблон перечисления кейса совпадает с кейсом существующего типа перечисления. Шаблоны перечисления кейса появляются в ярлыках кейсов инструкции switch и в условиях кейсов инструкции if, while, guard, и for-in.

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

Грамматика кейса перечисления

enum-case-pattern → type-identifieropt . enum-case-name tuple-patternopt

Шаблон опционала

Шаблон опционала совпадает со значениями, завернутыми в кейс some(Wrapped) перечисления Optional<Wrapped>. Опциональные шаблоны состоят из шаблона идентификатора, за которым сразу следует знак вопроса и встречаются они в тех же местах, где и шаблоны кейсов перечисления.

Поскольку опциональные шаблоны являются синтаксическим сахаром для Optional шаблонов кейсов перечисления, следующее является эквивалентным:

let someOptional: Int? = 42

// Сравнение с использованием шаблона кейса перечисления

if case .some(let x) = someOptional {

print(x)

}

// Сравнение с использованием шаблоном опционала

if case let x? = someOptional {

print(x)

}

Опциональный шаблон обеспечивает удобный способ перебора массива опциональных значений инструкции for-in, исполняя тело цикла только для элементов non-nil.

let arrayOfOptionalInts: [Int?] = [nil, 2, 3, nil, 5]

// Отметить только не nil значения

for case let number? in arrayOfOptionalInts {

print("Найдено \(number)")

}

// Найдено 2

// Найдено 3

// Найдено 5

Грамматика опционального шаблона

optional-pattern → identifier-pattern?

Шаблоны приведения типов

Существуют два шаблона приведения типов: шаблон is и шаблон as. Шаблон is появляется только в ярлыках кейса инструкции switch. Шаблоны is и as имеют следующий вид:

is тип

шаблон as тип

Шаблон is совпадает со значением, если тип этого значения во время выполнения программы является таким же, как и тип, указанный в правой части шаблона is или подклассе этого типа. Шаблон is ведет себя как оператор is в том, что они оба выполняют приведение типа, но отбрасывает возвращаемый тип.

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

Для примера того, как используется инструкция switch для совпадения значений с шаблонами is и as см. Приведение типов для Any и AnyObject.

Грамматика шаблона приведения типа

type-casting-pattern → is-pattern as-pattern

is-pattern → is type

as-pattern → pattern as type

Шаблон выражения

Шаблон выражения представляет собой значение выражения. Шаблоны выражения появляются только в ярлыках кейсов инструкции switch.

Выражение, представленное шаблоном выражения, сравнивается со значением входного выражения с помощью оператора стандартной библиотеки Swift ~= . Совпадение проходит успешно, если оператор ~=  возвращает true. По умолчанию оператор ~= сравнивает два значения одного и того же типа с помощью оператора ==. Также может быть совпадение со значением интеджера и диапазоном целых чисел в объекте Range, как показывает следующий пример:

let point = (1, 2)

switch point {

case (0, 0):

print("(0, 0) является точкой отсчета.")

case (-2...2, -2...2):

print("(\(point.0), \(point.1)) находится рядом с точкой отсчета.")

default:

print("Точка имеет координаты (\(point.0), \(point.1)).")

}

// Выведет "(1, 2)находится рядом с точкой отсчета."

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

// Перезагрузка оператора ~= для соответствия строке и целочисленному значению

func ~=(pattern: String, value: Int) -> Bool {

return pattern == "\(value)"

}

switch point {

case ("0", "0"):

print("(0, 0) является точкой отсчета.")

default:

print("Точка имеет координаты (\(point.0), \(point.1)).")

}

// Выведет "Точка имеет координаты (1, 2)."

Грамматика шаблона выражения

expression-pattern → expression

Параметры и аргументы универсального типа

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

Подробнее см. Универсальные шаблоны.

Условие универсального параметра (Generic Parameter Clause)

Generic Parameter Clause определяет параметры типа универсального типа или функции, наряду с любыми связанными ограничениями и требованиями для этих параметров. Условие универсального параметра заключается в угловые скобки (<>) и имеет одну из следующих форм:

< список параметров универсального типа >

Список универсальных параметров является разделенным запятыми списком универсальных параметров, каждый из которых имеет следующий вид:

параметра типа: ограничение

Универсальный параметр состоит из параметра типа, за которым следует опциональное ограничение. Параметр типа - это просто имя типа плейсхолдера (например, T, U, V, Key, Value, и так далее). У вас есть доступ к параметрам типа (и к любому из их связанных типов) в остальной части типа, функциям или объявлению инициализатора, в том числе и в подписи функции или инициализатора.

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

func simpleMax<T: Comparable>(_ x: T, _ y: T) -> T {

if x < y {

return y

}

return x

}

Так как Int и Double, например, оба соответвуют протоколу Comparable, эта функция принимает аргументы любого типа. В отличие от универсальных типов, вам не нужно указывать условие универсального аргумента при использовании универсальной функции или инициализатора. Аргументы типа же вместо этого выводятся из типа аргументов, переданных функции или инициализатору.

simpleMax(17, 42) // T определеятеся как Int

simpleMax(3.14159, 2.71828) // T определяется как Double

Условие Where

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

where требования

Требования в условии where указывают, что параметр типа наследует от класса или соответствует протоколу или композиции протокола. Несмотря на то, что условие where обеспечивает синтаксический сахар для выражения простых ограничений на параметры типа (например, T: Comparable эквивалентно <T> where T: Comparable и так далее), вы можете использовать его для обеспечения более сложных ограничений параметров типа и их связанных типов. Например, where S.Iterator.Element: Equatable указывает на то, что S соответствует протоколу Sequence и что связанный тип S.Iterator.Element соответствует протоколу Equatable. Данное ограничение проверяет, чтобы каждый элемент последовательности можно было сравнить.

Вы можете также указать требование, что два типа должны быть идентичны, с помощью оператора == . Например, условие универсального параметра <S1: Sequence, S2: Sequence where S1.Iterator.Element == S2.Iterator.Element> выражает ограничения, где S1 и S2 должны соответствовать протоколу Sequence и что элементы обеих последовательностей должны быть одного и того же типа.

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

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

Для получения дополнительной информации об универсальной оговорке where и примера ее в объявлении универсальной функции, см. Оговорка where.

Грамматика условия универсального параметра

generic-parameter-clause → <generic-parameter-list> generic-parameter-list → generic-parameter  | generic-parameter,generic-parameter-list generic-parameter → type-name generic-parameter → type-name:type-identifier generic-parameter → type-name:protocol-composition-type generic-where-clause → whererequirement-list requirement-list → requirement  | requirement,requirement-list requirement → conformance-requirement  | same-type-requirement conformance-requirement → type-identifier : type-identifier conformance-requirement → type-identifier : protocol-composition-type same-type-requirement → type-identifier == type

Условие универсального аргумента

Условие универсального аргумента определяет аргументы типа универсального типа. Условие универсального аргумента заключается в угловые скобки (<>) и имеет следующий вид:

<список универсальных аргументов>

Список универсальных аргументов - это разделенный запятыми список аргументов типа. Тип аргумента - это имя фактического конкретного типа, который заменяет соответствующий параметр типа в условии универсального аргумента. Результатом является специализированная версия этого универсального типа. В качестве примера, стандартная библиотека Swift определяет универсальный словарь типа вот так:

struct Dictionary<Key: Hashable, Value>: Collection, ExpressibleByDictionaryLiteral {

/* ... */

}

Специализированная версия универсального типа Dictionary,  Dictionary<String, Int>  формируется через замену универсальных параметров Key: Hashable и Value конкретными аргументами типа String и Int. Каждый аргумент типа должен удовлетворять всем ограничениями универсального параметра, которые он заменяет, в том числе дополнительным требованиям, указанным в условии where. В приведенном выше примере параметр типа Key ограничен для соответствия протоколу Hashable и, следовательно, String должна также соответствовать протоколу Hashable.

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

let arrayOfArrays: Array<Array<Int>> = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

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

Грамматика условия универсального аргумента

generic-argument-clause → < generic-argument-list >

generic-argument-list → generic-argument | generic-argument, generic-argument-list

generic-argument → type

Резюме по грамматике

Лексическая структура

Грамматика пустого пространства

whitespace → whitespace-item whitespace opt

whitespace-item → line-break

whitespace-item → comment

whitespace-item → multiline-comment

whitespace-item → U+0000, U+0009, U+000B, U+000C, or U+0020

line-break → U+000A

line-break → U+000D

line-break → U+000D followed by U+000A

comment → // comment-text line-break

multiline-comment → /* multiline-comment-text */

comment-text → comment-text-item comment-text opt

comment-text-item → Any Unicode scalar value except U+000A or U+000D

multiline-comment-text → multiline-comment-text-item multiline-comment-text opt

multiline-comment-text-item → multiline-comment

multiline-comment-text-item → comment-text-item

multiline-comment-text-item → Any Unicode scalar value except /* or */

Грамматика идентификатора

identifier → identifier-head identifier-charactersopt

identifier → `identifier-head identifier-charactersopt`

identifier → implicit-parameter-name

identifier-list → identifier identifier,identifier-list

identifier-head → Upper- or lowercase letter A through Z

identifier-head → _

identifier-head → U+00A8, U+00AA, U+00AD, U+00AF, U+00B2–U+00B5, or U+00B7–U+00BA

identifier-head → U+00BC–U+00BE, U+00C0–U+00D6, U+00D8–U+00F6, or U+00F8–U+00FF

identifier-head → U+0100–U+02FF, U+0370–U+167F, U+1681–U+180D, or U+180F–U+1DBF

identifier-head → U+1E00–U+1FFF

identifier-head → U+200B–U+200D, U+202A–U+202E, U+203F–U+2040, U+2054, or U+2060–U+206F

identifier-head → U+2070–U+20CF, U+2100–U+218F, U+2460–U+24FF, or U+2776–U+2793

identifier-head → U+2C00–U+2DFF or U+2E80–U+2FFF

identifier-head → U+3004–U+3007, U+3021–U+302F, U+3031–U+303F, or U+3040–U+D7FF

identifier-head → U+F900–U+FD3D, U+FD40–U+FDCF, U+FDF0–U+FE1F, or U+FE30–U+FE44

identifier-head → U+FE47–U+FFFD

identifier-head → U+10000–U+1FFFD, U+20000–U+2FFFD, U+30000–U+3FFFD, or U+40000–U+4FFFD

identifier-head → U+50000–U+5FFFD, U+60000–U+6FFFD, U+70000–U+7FFFD, or U+80000–U+8FFFD

identifier-head → U+90000–U+9FFFD, U+A0000–U+AFFFD, U+B0000–U+BFFFD, or U+C0000–U+CFFFD

identifier-head → U+D0000–U+DFFFD or U+E0000–U+EFFFD

identifier-character → Цифры от 0 до 9 (включительно)

identifier-character → U+0300–U+036F, U+1DC0–U+1DFF, U+20D0–U+20FF, or U+FE20–U+FE2F

identifier-character → identifier-head

identifier-characters → identifier-characteridentifier-charactersopt

implicit-parameter-name → $decimal-digits

Грамматика литерала

literal → numeric-literal | string-literal| boolean-literal | nil-literal

numeric-literal → -opt integer-literal | -optfloating-point-literal

boolean-literal → true | false

nil-literal → nil

Грамматика целочисленного литерала

integer-literal → binary-literal

integer-literal → octal-literal

integer-literal → decimal-literal

integer-literal → hexadecimal-literal

binary-literal → 0b binary-digitbinary-literal-charactersopt

binary-digit → Цифры 0 или 1

binary-literal-character → binary-digit | _

binary-literal-characters → binary-literal-character binary-literal-charactersopt

octal-literal → 0o octal-digitoctal-literal-charactersopt

octal-digit → Цифры от 0 до 7 включительно

octal-literal-character → octal-digit |  _

octal-literal-characters → octal-literal-character octal-literal-charactersopt

decimal-literal → decimal-digit decimal-literal-charactersopt

decimal-digit → Цифры от 0 до 9 включительно

decimal-digits → decimal-digitdecimal-digitsopt

decimal-literal-character → decimal-digit  | _

decimal-literal-characters → decimal-literal-character decimal-literal-charactersopt

hexadecimal-literal → 0x hexadecimal-digithexa decimal-literal-charactersopt

hexadecimal-digit → Цифры от 0 до 9 включительно, от a до f включительно или от A до F включительно

hexadecimal-literal-character → hexadecimal-digit | _

hexadecimal-literal-characters → hexadecimal-literal-character hexadecimal-literal-charactersopt

Грамматика числа с плавающей точкой

floating-point-literal → decimal-literal decimal-fractionopt decimal-exponentopt

floating-point-literal → hexadecimal-literal hexadecimal-fractionopt hexadecimal-exponent

decimal-fraction → .decimal-literal

decimal-exponent → floating-point-esignopt decimal-literal

hexadecimal-fraction → .hexadecimal-digit hexadecimal-literal-charactersopt

hexadecimal-exponent → floating-point-psignopt decimal-literal

floating-point-e → e | E

floating-point-p → p | P

sign → + | -

Грамматика строкового литерала

string-literal → static-string-literal | interpolated-string-literal

static-string-literal → " quoted-text opt "

static-string-literal → """ multiline-quoted-text opt """

quoted-text → quoted-text-item quoted-text opt

quoted-text-item → escaped-character

quoted-text-item → Any Unicode scalar value except ", \, U+000A, or U+000D "

multiline-quoted-text → multiline-quoted-text-item multiline-quoted-text opt

multiline-quoted-text-item → escaped-character

multiline-quoted-text-item → Any Unicode scalar value except \

multiline-quoted-text-item → escaped-newline

interpolated-string-literal → " interpolated-text opt "

interpolated-string-literal → """ multiline-interpolated-text opt """

interpolated-text → interpolated-text-item interpolated-text opt

interpolated-text-item → \( expression ) | quoted-text-item

multiline-interpolated-text → multiline-interpolated-text-item multiline-interpolated-text opt

multiline-interpolated-text-item → \( expression ) | multiline-quoted-text-item

escaped-character → \0 | \\ | \t | \n | \r | \" | \'

escaped-character → \u { unicode-scalar-digits }

unicode-scalar-digits → Between one and eight hexadecimal digits

escaped-newline → \ whitespace opt line-break

Грамматика операторов

operator → operator-head operator-charactersopt

operator → dot-operator-head dot-operator-characters

operator-head → / | = | - | + | ! | * | % | < | > | & | | | ^ | ~ | ?

operator-head → U+00A1–U+00A7

operator-head → U+00A9 or U+00AB

operator-head → U+00AC or U+00AE

operator-head → U+00B0–U+00B1, U+00B6, U+00BB, U+00BF, U+00D7, or U+00F7

operator-head → U+2016–U+2017 or U+2020–U+2027

operator-head → U+2030–U+203E

operator-head → U+2041–U+2053

operator-head → U+2055–U+205E

operator-head → U+2190–U+23FF

operator-head → U+2500–U+2775

operator-head → U+2794–U+2BFF

operator-head → U+2E00–U+2E7F

operator-head → U+3001–U+3003

operator-head → U+3008–U+3030

operator-character → operator-head

operator-character → U+0300–U+036F

operator-character → U+1DC0–U+1DFF

operator-character → U+20D0–U+20FF

operator-character → U+FE00–U+FE0F

operator-character → U+FE20–U+FE2F

operator-character → U+E0100–U+E01EF

operator-characters → operator-character operator-charactersopt

dot-operator-head → .

dot-operator-character → . | operator-character

dot-operator-characters → dot-operator-characterdot-operator-charactersopt

binary-operator → operator

prefix-operator → operator

postfix-operator → operator

Типы

Грамматика типа

type → array-type

type → dictionary-type

type → function-type

type → type-identifier

type → tuple-type

type → optional-type

type → implicitly-unwrapped-optional-type

type → protocol-composition-type

type → metatype-type

type → Any

type → Self

type → ( type )

Грамматика аннотации типа

type-annotation → :attributesopt  inoutopt type

Грамматика идентификатора типа

type-identifier → type-name  generic-argument-clauseopt  |  type-name  generic-argument-clauseopt  . type-identifier type-name → identifier

Грамматика кортежного типа

tuple-type → (tuple-type-bodyopt) tuple-type-element-list → tuple-type-element  |  tuple-type-element,tuple-type-element-list tuple-type-element →  element-name   type-annotation  |  type element-name → identifier

Грамматика функционального типа

function-type → attributesopt  function-type-argument-clause  throwsopt  ->  type function-type → attributesopt  function-type-argument-clause  rethrows  ->  type function-type-argument-clause → () function-type-argument-clause → (function-type-argument-list...opt) function-type-argument-list → function-type-argument | function-type-argument , function-type-argument-list function-type-argument → attributesopt  inoutopt  type | argument-label  type-annotation argument-label → identifier

Грамматика типа массива

array-type → [type]

Грамматика типа словаря

dictionary-type → [type : type]

Грамматика опционального типа

optional-type → type  ?

Грамматика неявно извлеченного опционального типа

implicitly-unwrapped-optional-type → type !

Грамматика типа композиции протоколов

protocol-composition-type → protocol-identifier & protocol-composition-continuation protocol-composition-continuation → protocol-identifier  | protocol-composition-type

Грамматика метатипа

metatype-type → type . Type  |  type.  Protocol

Грамматика наследования типа

type-inheritance-clause → : type-inheritance-list

type-inheritance-list → type-identifier | type-identifier, type-inheritance-list

Выражения

Грамматика выражений

expression → try-operator opt prefix-expression binary-expressionsopt

expression-list → expression  | expression,expression-list

Грамматика префиксных выражений

prefix-expression → prefix-operatoropt postfix-expression

prefix-expression → in-out-expression

in-out-expression → & identifier

Грамматика выражения try

try-operator → try | try? | try!

Грамматика бинарных выражений

binary-expression → binary-operator prefix-expression

binary-expression → assignment-operator try-operatoropt prefix-expression

binary-expression → conditional-operator try-operatoropt prefix-expression

binary-expression → type-casting-operator

binary-expressions → binary-expression binary-expressionsopt

Грамматика оператора присваивания

assignment-operator → =

Грамматика условного оператора

conditional-operator → ? try-operatoropt  expression:

Грамматика операторов приведения типа

type-casting-operator → is type type-casting-operator → as type type-casting-operator → as ? type type-casting-operator → as ! type

Грамматика первичных выражений

primary-expression → identifier generic-argument-clause opt

primary-expression → literal-expression

primary-expression → self-expression

primary-expression → superclass-expression

primary-expression → closure-expression

primary-expression → parenthesized-expression

primary-expression → tuple-expression

primary-expression → implicit-member-expression

primary-expression → wildcard-expression

primary-expression → key-path-expression

primary-expression → selector-expression

primary-expression → key-path-string-expression

Грамматика выражения литерала

literal-expression → literal

literal-expression → array-literal  | dictionary-literal |  playground-literal

literal-expression → #file  | #line | #column  |  #function array-literal → [array-literal-itemsopt] array-literal-items → array-literal-item,opt  array-literal-item,array-literal-items array-literal-item → expression dictionary-literal → [dictionary-literal-items] |[:] dictionary-literal-items → dictionary-literal-item,opt dictionary-literal-item,dictionary-literal-items dictionary-literal-item → expression:expression playground-literal → #colorLiteral ( red : expression, green : expression, blue : expression, alpha : expression) playground-literal → #fileLiteral ( resourceName : expression) playground-literal → #imageLiteral ( resourceName : expression)

Грамматика выражения self

self-expression → self  self-method-expression  | self-subscript-expression | self-initializer-expression self-method-expression → self.identifier self-subscript-expression → self[expression-list] self-initializer-expression → self.init

Грамматика выражения суперкласса

superclass-expression → superclass-method-expression  | superclass-subscript-expression | superclass-initializer-expression

superclass-method-expression → super.identifier superclass-subscript-expression → super[expression-list] superclass-initializer-expression → super.init

Грамматика выражений замыканий

closure-expression → {closure-signature opt statements}

‌closure-signature → capture-list opt closure-parameter-clause throws  opt function-result opt in

‌closure-signature → capture-list in

‌closure-parameter-clause → ( ) | (closure-parameter-list) | identifier-list

‌closure-parameter-list → closure-parameter | closure-parameter, closure-parameter-list

‌closure-parameter → closure-parameter-nametype-annotation opt

‌closure-parameter → closure-parameter-nametype-annotation...

‌closure-parameter-name → identifier

‌capture-list → [capture-list-items]

‌capture-list-items → capture-list-item capture-list-item, capture-list-items

‌capture-list-item → capture-specifier opt expression

‌capture-specifier → weak  | unowned | unowned(safe) |  unowned(unsafe)

Грамматика неявного выражения члена

implicit-member-expression → .identifier

Грамматика выражения в скобках

parenthesized-expression → (expression)

Грамматика выражения кортежа

tuple-expression → () | (tuple-element,tuple-element-list) tuple-element-list → tuple-element  | tuple-element,tuple-element-list tuple-element → expression  | identifier:expression

Грамматика выражения wildcard

wildcard-expression → _

Грамматика выражения Key-Path

key-path-expression → \ type opt . key-path-components

key-path-components → key-path-component | key-path-component . key-path-components

key-path-component → identifier key-path-postfixes opt | key-path-postfixes

key-path-postfixes → key-path-postfix key-path-postfixes opt

key-path-postfix → ? | ! | [ function-call-argument-list ]

Грамматика выражения селектора

selector-expression → #selector ( expression )

‌selector-expression → #selector ( getter: expression )

‌selector-expression → #selector ( setter: expression )

Грамматика выражения зачения пути по ключу

key-path-expression → #keyPath ( expression )

Грамматика постфиксных выражений

postfix-expression → primary-expression

postfix-expression → postfix-expressionpostfix-operator

postfix-expression → function-call-expression

postfix-expression → initializer-expression

postfix-expression → explicit-member-expression

postfix-expression → postfix-self-expression

postfix-expression → dynamic-type-expression

postfix-expression → subscript-expression

postfix-expression → forced-value-expression

postfix-expression → optional-chaining-expression

Грамматика выражения вызова функции

function-call-expression → postfix-expression function-call-argument-clause

function-call-expression → postfix-expression parenthesized-expressionopt trailing-closure

function-call-argument-clause → ( ) | (function-call-argument-list)

function-call-argument-list → function-call-argument | function-call-argument ,function-call-argument-list

function-call-argument → expression | identifier : expression

function-call-argument → operator | identifier : operator

trailing-closure → closure-expression

Грамматика выражения инициализатора

initializer-expression → postfix-expression.init initializer-expression → postfix-expression.init(argument-names)

Грамматика явного выражения члена

explicit-member-expression → postfix-expression.decimal-digits explicit-member-expression → postfix-expression.identifiergeneric-argument-clauseopt explicit-member-expression → postfix-expression.identifier(argument-names) argument-names → argument-name argument-namesopt argument-name → identifier:

Грамматика выражения self

postfix-self-expression → postfix-expression.self

Грамматика выражения сабскрипта

subscript-expression → postfix-expression [ expression-list ]

Грамматика принудительного значения

forced-value-expression → postfix-expression!

Грамматика принудительного значения

forced-value-expression → postfix-expression!

Грамматика выражения опциональной последовательности

optional-chaining-expression → postfix-expression?

Инструкции

Грамматика инструкций

statement → expression;opt statement → declaration;opt statement → loop-statement;opt statement → branch-statement;opt statement → labeled-statement;opt statement → control-transfer-statement;opt statement → defer-statement;opt statement → do-statement:opt statement → compiler-control-statement statements → statement statements opt

Грамматика инструкции цикла

loop-statement → for-in-statement

loop-statement → while-statement

loop-statement → repeat-while-statement

Грамматика инструкции for-in

for-in-statement → for caseopt pattern in expression where-clause opt code-block

Грамматика инструкции while

while-statement → while condition-clause code-block condition-clause → expression condition-clause → expression,condition-list condition-clause → condition-list condition-clause → availability-condition,expression condition-list → condition  |  condition,condition-list condition → availability-condition  |  case-condition  |  optional-binding-condition case-condition → case patterninitializerwhere-clauseopt optional-binding-condition → optional-binding-headoptional-binding-continuation-listoptwhere-clauseopt optional-binding-head → let patterninitializer  | var patterninitializer optional-binding-continuation-list → optional-binding-continuation optional-binding-continuation,optional-binding-continuation-list optional-binding-continuation → patterninitializer  | optional-binding-head

Грамматика repeat-while инструкции

repeat-while-statement → repeat code-block while expression

Грамматика инструкций ветвления

branch-statement → if-statement

branch-statement → guard-statement

branch-statement → switch-statement

Грамматика инструкции if

if-statement → if condition-clause   code-block  else-clause opt else-clause → else code-block  |  else if-statement

Грамматика инструкции guard

guard-statement → guard condition-clause else code-block

Грамматика инструкции switch

switch-statement → switch expression{switch-casesopt} switch-cases → switch-case switch-cases opt switch-case → case-label statements  |  default-label statements case-label → case case-item-list: case-item-list → pattern where-clauseopt  |  pattern where-clauseopt,case-item-list default-label → default: where-clause → where where-expression where-expression → expression

Грамматика маркированных инструкций

labeled-statement → statement-label loop-statement  |  statement-label if-statement  |  statement-labelswitch-statement

statement-label → label-name: label-name → identifier

Грамматика инструкции передачи управления

control-transfer-statement → break-statement

control-transfer-statement → continue-statement

control-transfer-statement → fallthrough-statement

control-transfer-statement → return-statement

control-transfer-statement → throw-statement

Грамматика инструкции Break

break-statement → break label-name opt

Грамматика инструкции Continue

continue-statement → continue label-name opt

Грамматика инструкции fallthrough

fallthrough-statement → fallthrough

Грамматика инструкции return

return-statement → return expression opt

Грамматика инструкции throw

throw-statement → throw expression

Грамматика инструкции defer

defer-statement → defer code-block

Грамматика инструкции do

do-statement → do code-block catch-clauses opt catch-clauses → catch-clause catch-clauses opt catch-clause → catch pattern opt where-clause opt code-block

Грамматика инструкций управления компилятором

compiler-control-statement → build-configuration-statement

compiler-control-statement → line-control-statement

Грамматика блока условной компиляции

conditional-compilation-block → if-directive-clause elseif-directive-clausesopt else-directive-clauseopt endif-directive

‌ if-directive-clause → if-directive compilation-condition statements opt

‌ elseif-directive-clauses → elseif-directive-clause elseif-directive-clauses opt

‌ elseif-directive-clause → elseif-directive compilation-condition statements opt

‌ else-directive-clause → else-directive statements opt

if-directive → #if

‌ elseif-directive → #elseif

‌ else-directive → #else

‌ endif-directive → #endif

‌ compilation-condition → platform-condition

‌ compilation-condition → identifier

‌ compilation-condition → boolean-literal

‌ compilation-condition → (compilation-condition)

‌ compilation-condition → !compilation-condition

‌ compilation-condition → compilation-condition && compilation-condition

‌ compilation-condition → compilation-condition || compilation-condition

‌ platform-condition → os (operating-system)

‌ platform-condition → arch (architecture)

‌ platform-condition → swift ( >= swift-version)

‌ operating-system → OSX  iOS  watchOS tvOS

‌ architecture → i386 | x86_64 |  arm |  arm64

‌ swift-version → decimal-digits . decimal-digits

Грамматика инструкции управления строкой

line-control-statement → #sourceLocation ( file: file-name,line:line-number)

‌ line-control-statement → #sourceLocation ( )

‌ line-number → Целое число большее нуля

‌ file-name → static-string-literal

Грамматика оператора диагностики времени компиляции

diagnostic-statement → #error ( diagnostic-message )

diagnostic-statement → #warning ( diagnostic-message )

diagnostic-message → static-string-literal

Грамматика условий доступности

availability-condition → #available ( availability-arguments )

availability-arguments → availability-argument  |  availability-argument , availability-arguments

‌availability-argument → platform-name platform-version

‌availability-argument → *

‌platform-name → iOS  |  iOSApplicationExtension

‌platform-name → OSX  |  OSXApplicationExtension

‌platform-name → watchOS

‌platform-name → tvOS

‌platform-version → decimal-digits

‌platform-version → decimal-digits . decimal-digits

‌platform-version → decimal-digits . decimal-digits . decimal-digits

Инструкции

Грамматика объявления

declaration → import-declaration

declaration → constant-declaration

declaration → variable-declaration

declaration → typealias-declaration

declaration → function-declaration

declaration → enum-declaration

declaration → struct-declaration

declaration → class-declaration

declaration → protocol-declaration

declaration → initializer-declaration

declaration → deinitializer-declaration

declaration → extension-declaration

declaration → subscript-declaration

declaration → operator-declaration

declarations → declarationdeclarations opt

Грамматика объявления верхнего уровня

top-level-declaration → statements opt

Грамматика блока кода

code-block → { statements opt }

Грамматика объявления импорта

import-declaration → attributes opt import import-kindopt import-path

import-kind → typealias | struct | class | enum | protocol | var | func

import-path → import-path-identifier | import-path-identifier . import-path

import-path-identifier → identifier | operator

Грамматика объявления константы

constant-declaration → attributes opt declaration-modifiers opt let pattern-initializer-list

pattern-initializer-list → pattern-initializer pattern-initializer , pattern-initializer-list

pattern-initializer → pattern initializer opt

initializer → = expression

Грамматика объявления переменной

variable-declaration → variable-declaration-head pattern-initializer-list

variable-declaration → variable-declaration-head variable-name type-annotation code-block

variable-declaration → variable-declaration-head variable-name type-annotation getter-setter-block

variable-declaration → variable-declaration-head variable-name type-annotation getter-setter-keyword-block

variable-declaration → variable-declaration-head variable-name initializer willSet-didSet-block

variable-declaration → variable-declaration-head variable-name type-annotation initializeropt willSet-didSet-block

variable-declaration-head → attributes opt declaration-modifiersopt var

variable-name → identifier

getter-setter-block → code-block

getter-setter-block → { getter-clause setter-clauseopt }

getter-setter-block → { setter-clause getter-clause }

getter-clause → attributesopt get code-block

setter-clause → attributesopt set setter-nameopt code-block

setter-name → ( identifier )

getter-setter-keyword-block → { getter-keyword-clause setter-keyword-clauseopt }

getter-setter-keyword-block → { setter-keyword-clause getter-keyword-clause }

getter-keyword-clause → attributesopt get

setter-keyword-clause → attributesopt set

willSet-didSet-block → { willSet-clause didSet-clauseopt }

willSet-didSet-block → { didSet-clause willSet-clauseopt }

willSet-clause → attributesopt willSet setter-nameopt code-block

didSet-clause → attributesopt didSet setter-nameopt code-block

Грамматика объявления псевдонима типа

typealias-declaration → attributesopt access-level-modifieropt typealias typealias-name typealias-assignment

typealias-name → identifier

typealias-assignment → = type

Грамматика объявления функции

function-declaration → function-head function-name generic-parameter-clauseopt function-signature function-bodyopt

‌function-head → attributesopt declaration-modifiersopt func

‌function-name → identifier | operator

‌function-signature → parameter-clause throwsopt function-resultopt

‌function-signature → parameter-clause rethrows function-resultopt

‌function-result →  -> attributesopt type

‌function-body → code-block

‌parameter-clause → () | ( parameter-list )

‌parameter-list → parameter | parameter , parameter-list

‌parameter → external-parameter-nameopt local-parameter-name type-annotation default-argument-clauseopt

parameter → external-parameter-nameopt local-parameter-name type-annotation

‌parameter → external-parameter-nameopt local-parameter-namet ype-annotation ...

‌external-parameter-name → identifier

‌local-parameter-name → identifier

‌default-argument-clause → = expression

Грамматика объявления перечислений

enum-declaration → attributesopt access-level-modifieropt union-style-enum

enum-declaration → attributesopt access-level-modifieropt raw-value-style-enum

union-style-enum → indirectopt enum enum-name generic-parameter-clauseopt type-inheritance-clauseopt { union-style-enum-membersopt }

union-style-enum-members → union-style-enum-member union-style-enum-membersopt

union-style-enum-member → declaration | union-style-enum-case-clause

union-style-enum-case-clause → attributesopt indirectopt case union-style-enum-case-list

union-style-enum-case-list → union-style-enum-case | union-style-enum-case , union-style-enum-case-list

union-style-enum-case → enum-case-name tuple-typeopt

enum-name → identifier

enum-case-name → identifier

raw-value-style-enum → enum enum-name generic-parameter-clauseopt type-inheritance-clause { raw-value-style-enum-members }

raw-value-style-enum-members → raw-value-style-enum-member raw-value-style-enum-membersopt

raw-value-style-enum-member → declaration raw-value-style-enum-case-clause

raw-value-style-enum-case-clause → attributesopt case raw-value-style-enum-case-list

raw-value-style-enum-case-list → raw-value-style-enum-case | raw-value-style-enum-case , raw-value-style-enum-case-list 

raw-value-style-enum-case → enum-case-name raw-value-assignmentopt

raw-value-assignment → = raw-value-literal

raw-value-literal → numeric-literal | static-string-literal | boolean-literal

Грамматика объявления структур

struct-declaration → attributesopt access-level-modifieropt struct - struct-name  generic-parameter-clauseopt  type-inheritance-clauseopt  generic-where-clauseopt struct-body struct-name → identifier struct-body → { struct-membersopt } struct-members → struct-memberstruct-membersopt struct-member → declaration  |  compiler-control-statement

Грамматика объявления класса

class-declaration → attributesopt  access-level-modifieropt  finalopt  class class-name  generic-parameter-clauseopt  type-inheritance-clauseopt  generic-where-clauseopt  class-body class-declaration → attributesopt  final access-level-modifieropt  class class-name  generic-parameter-clauseopt  type-inheritance-clauseopt  generic-where-clauseopt  class-body class-name → identifier class-body → { class-membersopt } class-members → class-member  class-membersopt class-member → declaration  |  compiler-control-statement

Грамматика объявления протокола

protocol-declaration → attributesopt access-level-modifieropt protocol protocol-name type-inheritance-clauseopt protocol-body protocol-name → identifier protocol-body → { protocol-member-declarationsopt } protocol-member-declaration → protocol-property-declaration protocol-member-declaration → protocol-method-declaration protocol-member-declaration → protocol-initializer-declaration protocol-member-declaration → protocol-subscript-declaration protocol-member-declaration → protocol-associated-type-declaration protocol-member-declarations → protocol-member-declaration protocol-member-declarationsopt

Грамматика объявления свойств протокола

protocol-property-declaration → variable-declaration-head  variable-name  type-annotation  getter-setter-keyword-block

Грамматика объявления метода протокола

protocol-method-declaration → function-head  function-name  generic-parameter-clauseopt   function-signature

Грамматика объявления инициализатора протокола

protocol-initializer-declaration → initializer-head  generic-parameter-clauseopt  parameter-clause throwsopt protocol-initializer-declaration → initializer-head  generic-parameter-clauseopt  parameter-clause rethrows

Грамматика объявления сабскрипта протокола

protocol-subscript-declaration → subscript-head subscript-result getter-setter-keyword-block

Грамматика объявления ассоциированного типа

protocol-associated-type-declaration → attributesopt access-level-modifieropt associatedtype typealias-name type-inheritance-clauseopt typealias-assignmentopt

Грамматика объявления инициализатора

initializer-declaration → initializer-head generic-parameter-clauseopt parameter-clause throwsopt initializer-body initializer-declaration → initializer-head generic-parameter-clauseopt parameter-clause rethrows initializer-body initializer-head → attributesopt declaration-modifiersopt init initializer-head → attributesopt declaration-modifiersopt init? initializer-head → attributesopt declaration-modifiersopt init! initializer-body → code-block

Грамматика объявления деинициализатора

deinitializer-declaration → attributesopt deinit code-block

Грамматика объявления расширений

extension-declaration → attributesopt  access-level-modifieropt  extension type-identifier  type-inheritance-clauseopt  extension-body extension-declaration → attributesopt  access-level-modifieropt  extension type-identifier  generic-where-clause  extension-body extension-body → { extension-membersopt } extension-members → extension-member  extension-membersopt extension-member → declaration  compiler-control-statement

Грамматика объявления сабскрипта

subscript-declaration → subscript-head subscript-result code-block

subscript-declaration → subscript-head subscript-result getter-setter-block

subscript-declaration → subscript-head subscript-resultgetter-setter-keyword-block

subscript-head → attributesopt declaration-modifiersopt subscript parameter-clause subscript-result → ->attributesopt type

Грамматика объявления оператора

operator-declaration → prefix-operator-declaration | postfix-operator-declaration | infix-operator-declaration prefix-operator-declaration → prefix operator operator postfix-operator-declaration → postfix operator operator infix-operator-declaration → infix operator operator infix-operator-attributesopt infix-operator-group → : precedence-group-name

operator-declaration → prefix-operator-declaration | postfix-operator-declaration | infix-operator-declaration prefix-operator-declaration → prefix operator operator postfix-operator-declaration → postfix operator operator infix-operator-declaration → infix operator operator infix-operator-attributesopt infix-operator-group → : precedence-group-name

Грамматика объявления групп приоритета

recedence-group-declaration → precedencegroup precedence-group-name { precedence-group-attributes opt } precedence-group-attributes → precedence-group-attribute precedence-group-attributes opt precedence-group-attribute → precedence-group-relation precedence-group-attribute → precedence-group-assignment precedence-group-attribute → precedence-group-associativity precedence-group-relation → higherThan : precedence-group-names precedence-group-relation → lowerThan : precedence-group-names precedence-group-assignment → assignment : boolean-literal precedence-group-associativity → associativity : left precedence-group-associativity → associativity : right precedence-group-associativity → associativity : none precedence-group-names → precedence-group-name  |  precedence-group-name,precedence-group-names precedence-group-name → identifier

Грамматика объявления модификатора

declaration-modifier → class | convenience | dynamic | final | infix | lazy | optional | override | postfix | prefix | required | static | unowned | unowned(safe) | unowned(unsafe) | weak declaration-modifier → access-level-modifier declaration-modifier → mutation-modifier declaration-modifiers → declaration-modifierdeclaration-modifiersopt access-level-modifier → private  | private(set) access-level-modifier → fileprivate | fileprivate(set) access-level-modifier → internal | internal(set) access-level-modifier → public | public(set) access-level-modifier → open | open(set) mutation-modifier → mutating | nonmutating

Атрибуты

Грамматика атрибута

attribute → @ attribute-nameattribute-argument-clauseopt attribute-name → identifier attribute-argument-clause → ( balanced-tokensopt ) attributes → attributeattributesopt balanced-tokens → balanced-tokenbalanced-tokensopt balanced-token → ( balanced-tokensopt ) balanced-token → [ balanced-tokensopt] balanced-token → { balanced-tokensopt } balanced-token → Any identifier, keyword, literal, or operator balanced-token → Any punctuation except (, ), [, ], {, or }

Шаблоны

Грамматика шаблонов

pattern → wildcard-pattern type-annotationopt

pattern → identifier-pattern type-annotationopt

pattern → value-binding-pattern

pattern → tuple-pattern type-annotationopt

pattern → enum-case-pattern

pattern → optional-pattern

pattern → type-casting-pattern

pattern → expression-pattern

Грамматика шаблона wildcard

wildcard-pattern → _

Грамматика шаблона идентификатора

identifier-pattern → identifier

Грамматика шаблона присваивающего значение

value-binding-pattern → var pattern let pattern

Грамматика шаблона кортежа

tuple-pattern → ( tuple-pattern-element-listopt )

tuple-pattern-element-list → tuple-pattern-element | tuple-pattern-element, tuple-pattern-element-list

tuple-pattern-element → pattern | identifier : pattern

Грамматика кейса перечисления

enum-case-pattern → type-identifieropt . enum-case-name tuple-patternopt

Грамматика опционального шаблона

optional-pattern → identifier-pattern?

Грамматика шаблона приведения типа

type-casting-pattern → is-pattern as-pattern

is-pattern → is type

as-pattern → pattern as type

Грамматика шаблона выражения

expression-pattern → expression

Параметры и аргументы универсального типа

Грамматика условия универсального параметра

generic-parameter-clause → <generic-parameter-list> generic-parameter-list → generic-parameter  | generic-parameter,generic-parameter-list generic-parameter → type-name generic-parameter → type-name:type-identifier generic-parameter → type-name:protocol-composition-type generic-where-clause → whererequirement-list requirement-list → requirement  | requirement,requirement-list requirement → conformance-requirement  | same-type-requirement conformance-requirement → type-identifier : type-identifier conformance-requirement → type-identifier : protocol-composition-type same-type-requirement → type-identifier == type

Грамматика условия универсального аргумента

generic-argument-clause → < generic-argument-list >

generic-argument-list → generic-argument | generic-argument, generic-argument-list

generic-argument → type