Sendable — це протокол Swift, який маркує типи, безпечні для передачі між потоками. Під час роботи з конкурентністю (actor, async/await, Task) компілятор Swift вимагає, щоб усі дані, що передаються між ізольованими контекстами, відповідали Sendable. Це виключає випадкову передачу небезпечних типів, які можуть призвести до станів гонки. Протокол виступає контрактом: тип, що відповідає Sendable, гарантує відсутність внутрішнього несинхронізованого стану. За даними WWDC 2021, протокол Sendable є обов'язковим елементом при проєктуванні безпечної багатопоточної архітектури.
Головне
Sendable — це протокол зі стандартної бібліотеки Swift (SE-0302), який маркує типи, безпечні для передачі між доменами ізоляції. Під доменом ізоляції розуміється actor, задача Task або @MainActor-контекст. Sendable не містить вимог — він виступає маркером безпеки.
До Swift 5.5 розробники могли передати будь-який об'єкт між потоками через DispatchQueue, і компілятор не перевіряв, чи це безпечно. Sendable закриває цю прогалину: тепер компілятор сам відстежує міжпотокові передачі та блокує небезпечні. Це робить конкурентний код надійнішим ще до його запуску.
struct UserProfile: Sendable {
let name: String
let age: Int
}
class NonSendableClass {
var counter: Int = 0
}
У прикладі UserProfile може бути безпечно переданий між потоками, оскільки це структура з константними властивостями Sendable-типів. NonSendableClass викличе помилку компіляції при спробі передачі через await або в Task.
Sendable-типи у Swift поділяються на три категорії: value-типи з Sendable-властивостями, final-класи з незмінним станом і функції/замикання, позначені @Sendable. Компілятор автоматично виводить відповідність Sendable для value-типів.
Struct, enum і tuple автоматично стають Sendable, якщо всі їхні властивості та асоційовані значення теж Sendable. Це консервативний підхід: якщо хоча б одна властивість не відповідає Sendable, весь тип не буде визнано безпечним.
| Тип | Sendable | Умова |
|---|---|---|
| Struct | Так (без явного зазначення) | Всі властивості Sendable |
| Enum | Так (без явного зазначення) | Всі асоційовані значення Sendable |
| Final class | Так (з явним зазначенням) | Всі let-властивості Sendable, немає var |
| Non-final class | Ні | Не може бути Sendable через успадкування |
Усі вбудовані типи Swift — Int, String, Double, Bool, Optional, Array, Dictionary, Set — відповідають Sendable. Це робить композицію типів безпечною за замовчуванням. Розробнику достатньо стежити за кастомними класами.
@unchecked Sendable — це механізм, що дозволяє класу явно оголосити себе Sendable, обходячи перевірки компілятора. Розробник бере на себе відповідальність за потокобезпеку такого класу. Це корисно для Objective-C-мостів та оптимізованих структур.
@unchecked Sendable застосовується, коли клас внутрішньо гарантує безпеку через блокування або атомарні операції, але компілятор не може це перевірити статично. Наприклад, клас із os_unfair_lock або pthread_mutex_t — його потокобезпека забезпечується кодом, але Swift не бачить цього.
final class AtomicCounter: @unchecked Sendable {
private var value: Int = 0
private let lock = NSLock()
func increment() {
lock.lock()
value += 1
lock.unlock()
}
}
Використовувати @unchecked Sendable слід обережно. Це явний сигнал іншим розробникам: «я перевірив, це безпечно». Помилка в реалізації може призвести до важковідловлюваних станів гонки. Перш ніж застосовувати @unchecked, варто переконатися, що тип дійсно не може бути переписаний як value-тип.
Actor і Sendable — дві сторони однієї медалі. Actor ізолює свій стан, але для обміну даними із зовнішнім світом він повинен повертати Sendable-типи. Якщо метод actor повертає не-Sendable тип, компілятор видає попередження або помилку.
Коли actor надсилає дані зовнішньому коду, ці дані перетинають межу ізоляції. Sendable гарантує, що отримувач може безпечно використовувати їх поза actor. Сам actor залишається ізольованим — його внутрішній стан не розкривається.
struct AccountSnapshot: Sendable {
let id: UUID
let balance: Double
let lastUpdated: Date
}
actor BankActor {
private var balance: Double = 0
func snapshot() async -> AccountSnapshot {
return AccountSnapshot(
id: UUID(),
balance: balance,
lastUpdated: Date()
)
}
}
AccountSnapshot — це Sendable-структура, яка містить тільки let-властивості Sendable-типів. Такий підхід — best practice для вилучення даних з actor. Знімок стану передається за значенням, і actor не втрачає контролю над своїм станом.
@Sendable — це атрибут для функцій і замикань, який гарантує, що замикання не захоплює не-Sendable дані у змінному вигляді. Коли замикання передається в Task або метод actor, воно має бути Sendable.
Компілятор перевіряє, що @Sendable замикання не захоплює змінні посилання на класи. Захоплення let-властивостей Sendable-типів дозволено. Захоплення var-змінної посилального типу призведе до помилки, оскільки замикання може виконуватися одночасно з мутацією.
func performAsync(operation: @Sendable () -> Void) {
Task {
await operation()
}
}
let constant = "Safe"
var counter = 0
// counter — захоплення var змінює counter, помилка компіляції
performAsync { // ❌ Мутація захопленої змінної
print(constant)
}
Правило просте: @Sendable замикання може захоплювати тільки ті дані, які самі є Sendable і не будуть мутуватися ззовні. Для класів допустиме захоплення слабкого посилання weak self, якщо клас не позначений як Sendable. Це запобігає класичним retain cycle та станам гонки.
Поширені запитання
Sendable — це маркер, який каже компілятору: «цей тип можна безпечно передавати між потоками». Структури та константи зазвичай Sendable за замовчуванням, класи — ні, якщо це не зазначено явно.
Value-типи (struct, enum) з Sendable-властивостями, final-класи з незмінним станом, а також усі базові типи Swift: Int, String, Double, Bool, Array, Dictionary, Optional — відповідають Sendable автоматично.
@unchecked Sendable — це спосіб оголосити клас Sendable без статичної перевірки компілятора. Розробник гарантує безпеку самостійно, часто через блокування або атомарні операції.
Actor ізолює свій стан, але при поверненні даних зовнішньому коду ці дані перетинають межу ізоляції. Sendable гарантує, що отримувач не зіткнеться зі станами гонки при використанні цих даних поза actor.
Додайте протокол Sendable до final-класу, всі властивості якого — константи Sendable-типів. Якщо клас використовує блокування, можна застосувати @unchecked Sendable, але це потребує обережності.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.