Swift 5.5: что нового. Sendable и другие улучшения. Часть третья
Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!
В предыдущем материале цикла мы разобрались с тем, как акторы помогают защитить общее состояние при многопоточном доступе. Теперь нужно разобраться с ещё одним методом, которым 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. Впрочем, если эти статьи будут интересны, я постараюсь рассказать о самых интересных нововведениях и там.
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів