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 | Да (без явного указания) | Все associated values 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 — capturing var mutates counter, compile error
performAsync { // ❌ Mutation of captured var
print(constant)
}
Правило простое: @Sendable замыкание может захватывать только те данные, которые сами являются Sendable и не будут мутироваться извне. Для классов допустим захват слабой ссылки weak self, если класс не помечен как Sendable. Это предотвращает классические retain cycle и race condition.
Часто задаваемые вопросы
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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также