Тур по Swift

17 ноября 2022

По традиции, первая программа на новом языке должна выводить на экран словосочетание «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 fruits = ["strawberries", "limes", "tangerines"]
fruits[1] = "grapes"

var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",
 ]
occupations["Jayne"] = "Public Relations"

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


fruits.append("blueberries")
print(fruits)
// Выведет "["strawberries", "grapes", "tangerines", "blueberries"]"

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


let emptyArray = [String]()
let emptyDictionary = [String: Float]()

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


fruits = []
occupations = [:]

 

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

Для создания условий используйте if и switch, а для создания циклов используйте for-inwhile, и 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.")
}
// Выведет "Is it a spicy red pepper?"

Задание

Попробуйте удалить блок 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 (_, 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 класса NamedShape, который принимает радиус и имя в качестве аргументов в их инициализатор. Реализуйте метод 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 имеет три разных шага:

  1. Устанавливает значение свойств объявляемых подклассом.
  2. Вызывает инициализатор родителя.
  3. Изменяет значение свойств, объявленных родителем. Любые дополнительные работы по начальной установке, которые используют методы, 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).

Параллелизм

Для того, чтобы отметить, что функция запускается асинхронно используйте async.

func fetchUserID(from server: String) async -> Int {
    if server == "primary" {
        return 97
    }
    return 501
}

Вы отмечаете вызов асинхронной функции, просто написав await перед нею.

func fetchUsername(from server: String) async -> String {
    let userID = await fetchUserID(from: server)
    if userID == 501 {
        return "John Appleseed"
    }
    return "Guest"
}

Используйте async let для вызова асинхронной функции, позволяя ей выполняться параллельно с другим асинхронным кодом. Если вы используете значение, которое возвращает эта функция, то напишите await.

func connectUser(to server: String) async {
    async let userID = fetchUserID(from: server)
    async let username = fetchUsername(from: server)
    let greeting = await "Hello \(username), user ID \(userID)"
    print(greeting)
}

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

Task {
    await connectUser(to: "primary")
}
// Выведет "Hello Guest, user ID 97"

 

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

Используйте оператор 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.

 
Содержание