Что нового в Swift 5.3

Туториалы

Что нового в Swift 5.3

Что нового в Swift 5.3?

Многошаблонные блоки catch, улучшенный Package Manager и другие дополнения для SwiftUI.

Swift 5.3 привносит в Swift некоторые дополнения, в числе которых такие полезные функции, как многошаблонные блоки catch, множественные последующие замыкания, а так же некоторые важные изменения для Swift Package Manager.

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

Многошаблонные блоки catch 

SE-0276 ввел возможность отслеживать сразу несколько ошибок внутри одного catch-блока, что позволяет устранить дублирование в логе ошибок.

Так, представим некоторый код, который содержит перечисление с двумя кейсами ошибок:

 

enum TemperatureError: Error {

    case tooCold, tooHot

}

При считывании температуры чего-либо, мы можем либо получить одну из этих ошибок, либо снова отправить “ОК”:

 

func getReactorTemperature() -> Int {

    90

}

func checkReactorOperational() throws -> String {

    let temp = getReactorTemperature()

    if temp < 10 {

        throw TemperatureError.tooCold

    } else if temp > 90 {

        throw TemperatureError.tooHot

    } else {

        return "OK"

    }

}

Как только дело доходит до вылавливания возникающих ошибок, SE-0276 позволит нам идентично обрабатывать как tooHot, так и tooCold, разделяя их запятой:

do {

    let result = try checkReactorOperational()

    print("Result: \(result)")

} catch TemperatureError.tooHot, TemperatureError.tooCold {

    print("Shut down the reactor!")

} catch {

    print("An unknown error occurred.")

}

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

Множественные последующие замыкания

SE-0279 ввел множественные последующие замыкания, что облегчает возможность вызова функции с несколькими последующими замыканиями.

Это будет особенно приветствоваться в SwiftUI, где будет код подобного типа:

struct OldContentView: View {

    @State private var showOptions = false

    var body: some View {

        Button(action: {

            self.showOptions.toggle()

        }) {

            Image(systemName: "gear")

        }

    }

}

Теперь можно делать так: 

struct NewContentView: View {

    @State private var showOptions = false

    var body: some View {

        Button {

            self.showOptions.toggle()

        } label: {

            Image(systemName: "gear")

        }

    }

}

Технически нет причин, по которым label: должен быть на той же строке, что и предыдущий } , так что вы можете написать так, если вам это необходимо:

struct BadContentView: View {

    @State private var showOptions = false

    var body: some View {

        Button {

            self.showOptions.toggle()

        }

        label: {

            Image(systemName: "gear")

        }

    }

}

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

Заметка

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

Синтезированное соответствие протоколу Comparable для перечислений

SE-0266 позволяет нам пропустить реализацию соответствия протоколу Comparable для перечислений, которые либо не имеют ассоциативных значений, либо имеют эти ассоциативные значения, соответствующие протоколу Comparable. Это позволяет нам сравнить два кейса из одного и того же перечисления с помощью операторов <, > и подобных.

Например, если бы у нас было перечисление, описывающее размеры одежды, мы могли бы попросить Swift синтезировать соответствие для Comparable следующим образом:

enum Size: Comparable {

    case small

    case medium

    case large

    case extraLarge

}

Теперь мы можем создать два экземпляра этого перечисления и сравнить их с помощью <, например:

let shirtSize = Size.small

let personSize = Size.large

if shirtSize < personSize {

    print("That shirt is too small")

}

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

enum WorldCupResult: Comparable {

    case neverWon

    case winner(stars: Int)

}

Затем мы могли бы создать несколько экземпляров этого перечисления с различными значениями и позволить Swift их сортировать:

let americanMen = WorldCupResult.neverWon

let americanWomen = WorldCupResult.winner(stars: 4)

let japaneseMen = WorldCupResult.neverWon

let japaneseWomen = WorldCupResult.winner(stars: 1)

let teams = [americanMen, americanWomen, japaneseMen, japaneseWomen]

let sortedByWins = teams.sorted()

print(sortedByWins)

Это упорядочит массив таким образом, что две команды, которые не выиграли чемпионат мира, придут первыми, затем японская женская команда, затем американская женская команда. Выходит,  два winner кейса будут выше, чем два neverWon кейса, и это значит, что  winner(stars: 4) будет выше winner(stars: 1).

Использование self во многих случаях будет не нужно 

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

Например, до этого изменения мы бы написали код следующим образом:

struct OldContentView: View {

    var body: some View {

        List(1..<5) { number in

            self.cell(for: number)

        }

    }

    func cell(for number: Int) -> some View {

        Text("Cell \(number)")

    }

}

Призыв функции self.cell(for:) не может вызвать цикл сильных ссылок, потому что она используется внутри структуры. Благодаря SE-0269 мы можем написать один и тот же код следующим образом:

struct NewContentView: View {

    var body: some View {

        List(1..<5) { number in

            cell(for: number)

        }

    }

    func cell(for number: Int) -> some View {

        Text("Cell \(number)")

    }

}

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

Основанная на типе точка входа

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

Например, при создании терминального приложения ранее нам нужно было создать файл с именем main.swift, способный запустить наш код:

struct OldApp {

    func run() {

        print("Running!")

    }

}

let app = OldApp()

app.run()

Swift автоматически считал, что код в main.swift является кодом верхнего уровня, а значит он отвечает за создание экземпляра App. Так и остается даже после SE-0281 однако, если вы хотите, вы можете удалить main.swift и вместо этого использовать атрибут @main для обозначения структуры или базового класса, содержащий статический метод main(), используемый в качестве точки запуска программы:

@main

struct NewApp {

    static func main() {

        print("Running!")

    }

}

Если мы запустим программу, Swift автоматически вызовет NewApp.main () для запуска вашего кода.

Новый атрибут @main будет понятен разработчикам UIKit и AppKit, в случае, когда мы используем @UIApplicationMain и @NSApplicationMain для обозначения наших делегатов приложения.

Однако есть некоторые нюансы, о которых вы должны знать при использовании @main:

  • Вы не можете использовать этот атрибут в приложении, которое уже имеет main.swift file.
  • У вас может быть не более одного атрибута @main
  • Атрибут @main может быть применен только к базовому классу – он не будет унаследован никакими подклассами.

Оговорка Where в контекстуальных универсальных объявлениях

SE-0267 ввел возможность прикреплять оговорку where к функциям внутри универсальных типов и расширений.

Например, мы могли бы начать с простой структуры Stack, которая позволяет нам извлекать значения из private массива:

struct Stack<Element> {

    private var array = [Element]()

    mutating func push(_ obj: Element) {

        array.append(obj)

    }

    mutating func pop() -> Element? {

        array.popLast()

    }

}

Используя SE-0267, мы могли бы добавить новый метод sorted() в этот Stack, но только для тех случаев, когда элементы внутри Stack соответствуют Comparable:

extension Stack {

    func sorted() -> [Element] where Element: Comparable {

        array.sorted()

    }

}

Кейсы перечисления в качестве Protocol Witness

SE-0280 позволяет перечислениям участвовать в реализации метода протокола, это позволяет сказать, что теперь они могут реализовать соответствие протоколу более просто.

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

protocol Defaultable {

    static var defaultValue: Self { get }

}

// make integers have a default value of 0

extension Int: Defaultable {

    static var defaultValue: Int { 0 }

}

// make arrays have a default of an empty array

extension Array: Defaultable {

    static var defaultValue: Array { [] }

}

// make dictionaries have a default of an empty dictionary

extension Dictionary: Defaultable {

    static var defaultValue: Dictionary { [:] }

}

То, что SE-0280 позволяет нам делать - это то же самое только для перечислений. Например, вы хотите создать перечисление Padding, которое может принимать некоторое количество пикселей, некоторое количество сантиметров или исходное значение, определяемое системой:

enum Padding: Defaultable {

    case pixels(Int)

    case cm(Int)

    case defaultValue

}

Такого рода код был бы невозможен до SE-0280. Swift считал бы, что Padding не удовлетворяет протоколу. Однако, если вы посмотрите с точки зрения протокола, то его требования удовлетворены: потому что протоколу требуется статическое значение defaultValue, которое возвращает Self, то есть любой конкретный тип, соответствующий протоколу, и это именно то, что Padding.defaultValue реализует.

Изящная семантика didSet

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

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

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

didSet {

    _ = oldValue

}

Новый тип Float16

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

Этот новый тип с плавающей точкой вписывается в другие аналогичные типы Swift:

let first: Float16 = 5

let second: Float32 = 11

let third: Float64 = 7

let fourth: Float80 = 13

Swift Package Manager получает бинарные зависимости, ресурсы и многое другое

Swift 5.3 представил множество улучшений для Swift Package Manager (SPM). Хотя здесь нет возможности привести практические примеры, мы можем, по крайней мере, обсудить, что изменилось и почему.

  1. Во-первых, SE-0271 позволяет SPM содержать такие ресурсы, как изображения, аудио, JSON и многое другое. Это больше, чем просто копирование файлов в готовый пакет приложений – например, мы можем применить кастомный шаг обработки к нашим каталогам изображений, например оптимизировать изображения для iOS. Это также добавляет новый Bundle.module для доступа к этим каталогам во время выполнения. SE-0278 (Package Manager Localised Resources) строится на том, чтобы разрешить использование локализованных версий ресурсов, например изображений на французском языке.
  1. Во-вторых, SE-0272 (Package Manager Binary Dependencies) позволяет SPM использовать бинарные пакеты наряду с существующей поддержкой исходных пакетов. Это означает, что общие SDK с закрытым исходным кодом, такие как Firebase, теперь могут быть интегрированы с помощью SPM.
  1. В-третьих, SE-0273 (Package Manager Conditional Target Dependencies) позволяет настроить целевые объекты так, чтобы они имели зависимости только для определенных платформ и конфигураций. Например, мы можем сказать, что нам нужны некоторые специальные дополнительные фреймворки при компиляции для Linux, или что мы должны создать некоторый отладочный код при компиляции для локального тестирования.
  1. Стоит добавить, что в разделе “Future Directions” SE-0271 упоминается возможность типобезопасного доступа к отдельным исходным файлам – возможность для SPM генерировать конкретные объявления для наших исходных файлов в виде Swift-кода, что означает, что такие вещи, как Image("avatar"), становятся чем-то вроде Image(module.avatar).

На этом мы завершаем обзор изменений в Swift 5.3.

Оригинал статьи

Спасибо за внимание!

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: