Swift 5.5: что нового. Sendable и другие улучшения. Часть третья

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

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

Sendable и @Sendable

Очевидно, что в Swift есть типы данных, которые можно безопасно передавать с одного потока исполнения в другой, не опасаясь коллизий. В SE-0302 вводится чёткий способ отделить «овец от козлищ»: концепция sendable типов данных. Чтоб отметить подобные безопасные типы был введён маркерный протокол Sendable.

Из встроенных типов данных к «пересылаемым» (то есть тем, что можно безопасно отправлять за границы одного потока) относятся:

  • «базовые» типы данных (числа, строки, bool и прочие)
  • опционалы, если завёрнутый в них тип данных обладает value семантикой
  • таплы с элементами являющимися value types
  • коллекции, если в них хранятся value типы (например, Array<Int> и Dictionary<Int, String>)
  • метатипы (например, Int.self)

Определённые пользователем типы данных часто тоже могут быть sendable

  • акторы автоматически соответствуют Sendable, поскольку они синхронизируют доступ к своему состоянию
  • структуры и перечисления автоматически «получают» данный протокол если все типы данных что в них хранятся — sendable. Примерно так же как это работает с Codable
  • классы тоже могут быть Sendable, для этого им надо соответствовать следующим критериям: не иметь предка или наследоваться от NSObject, не разрешать наследование, используя ключевое слово final и иметь только константные поля sendable типов.

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

final class Receipt: Sendable {
    let name: String
    let amount: Decimal

    init(name: String, amount: Decimal) {
        self.name = name
        self.amount = amount
    }
}

Вот пример «безопасного» класса. Если вдруг вам понадобится написать класс, обеспечивающий потокобезопасность с помощью внутренних методов, отметить его как Sendable не получится, если не выполнены три вышеоговоренных условия. Для решения проблемы есть протокол UnsafeSendable, сообщающий компилятору, что вы берёте ответственность на себя. Но уже сейчас он отмечен как deprecated и вместо него предлагается использовать @unchecked Sendable. В общем, жить с бетами — интересно.

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

struct User: Sendable {
    let name: String
    let accountLabelText: NSMutableAttributedString
}

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

private func doItLater(_ job: @escaping @Sendable () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: job)
}

После этого уже не выйдет написать что-то вроде такого:

private func procrastinate() {
    var someVal = 1

    doItLater {
        print(someVal)
    }
}

Компилятор справедливо упрекнёт нас в том что мы захватываем переменную в замыкание, которое может выполняться асинхронно, создавая при этом возможности для конфликта.

Автогенерация Codable для перечислений с ассоциированными значениями

Дальше пройдёмся по нововведениям, которые не так обширны, но при этом довольно приятны. В SE-0295 завезли автоматическую поддержку кодирования/раскодирования enums with associated values. Теперь что-то подобное можно автоматически кодировать.

enum Command: Codable {
    case finish
    case left(Double)
    case right(Double)
    case step(count: Int)
}

let routine: [Command] = [
    .step(count: 5),
    .left(10.5),
    .step(count: 10),
    .finish
]

Дамп routine в JSON выглядит так.

[{
    "step": {
        "count": 5
    }
}, {
    "left": {
        "_0": 10.5
    }
}, {
    "step": {
        "count": 10
    }
}, {
    "finish": {}
}]

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

Взаимозаменяемость CGFloat и Double

Хорошо когда поддержку 32 бит можно уже оставить в прошлом. Естественно, необходимость принудительного приведения CGFloat и Double — не самая большая проблема Swift, но порой при работе с графикой это раздражало. Теперь эта проблема в прошлом и эти два типа полностью взаимозаменяемы, спасибо SE-0307.

let oldWidth: CGFloat = 200.0
let multiplier: Double = 1.3
let newWidth = oldWidth * multiplier

Property wrappers применимы к параметрам функций и замыканий

Идея SE-0293 полностью раскрывается в названии. Допустим, у нас есть какой-то простой враппер.

@propertyWrapper
struct NonNegative<T: Numeric & Comparable> {
    let wrappedValue: T

    init(wrappedValue: T) {
        self.wrappedValue = max(0, wrappedValue)
    }
}

Теперь допускается следующий синтаксис.

func process(@NonNegative number: Int) {
    print("Handling \(number)")
}

Работает это ожидаемо. Вызов process(number: 10) выдаст Handling 10, а process(number: -5) — Handling 0.

lazyработает в локальном контексте.

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

func doHeavyLifting() -> String {
    print("Some complex work")

    return "Done"
}

func asLazyAsMe() {
    lazy var result = doHeavyLifting()
    print("No call yet")
    print("Result is \(result)")
}

Результат выполнения будет ожидаемым.

No call yet
Some complex work
Result is Done

Static method lookup работает и с дженериками

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

Toggle("Remember me", isOn: $isRememberMeEnabled)
  .toggleStyle(SwitchToggleStyle())

После реализации SE-0299 синтаксис упростился.

Toggle("Remember me", isOn: $isRememberMeEnabled)
  .toggleStyle(.switch)

#if внутри member expressions

Пожалуй и SE-0308 больше всего пригодится в SwiftUI, ведь там длинные цепочки вызовов особенно часто используются для настройки каких-либо параметров View. Теперь там можно использовать различные условия компиляции.

Text("Hello")
#if DEBUG
    .foregroundColor(.red)
#endif

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

let result = [1, 2, 3]
#if os(iOS)
    .map { $0 * 2}
#else
    .reduce(0, +)
#endif

print(result)

Заключение

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

👍НравитсяПонравилось9
В избранноеВ избранном5
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Подписаться на комментарии