Sendable — to protokół Swifta, który oznacza typy bezpieczne do przesyłania między wątkami. Podczas pracy ze współbieżnością (actor, async/await, Task) kompilator Swift wymaga, aby wszystkie dane przesyłane między izolowanymi kontekstami były zgodne z Sendable. Eliminuje to przypadkowe przesyłanie niebezpiecznych typów, które mogą prowadzić do wyścigów danych. Protokół pełni rolę kontraktu: typ zgodny z Sendable gwarantuje brak wewnętrznego niesynchronizowanego stanu. Według WWDC 2021, protokół Sendable jest obowiązkowym elementem przy projektowaniu bezpiecznej architektury wielowątkowej.
Najważniejsze
Sendable — to protokół ze standardowej biblioteki Swift (SE-0302), który oznacza typy bezpieczne do przesyłania między domenami izolacji. Domeną izolacji jest actor, zadanie Task lub kontekst @MainActor. Sendable nie zawiera wymagań — pełni rolę znacznika bezpieczeństwa.
Przed Swift 5.5 deweloperzy mogli przesłać dowolny obiekt między wątkami przez DispatchQueue, a kompilator nie sprawdzał, czy jest to bezpieczne. Sendable zamyka tę lukę: teraz kompilator sam śledzi przesłania międzywątkowe i blokuje niebezpieczne. To sprawia, że kod współbieżny jest bardziej niezawodny jeszcze przed uruchomieniem.
struct UserProfile: Sendable {
let name: String
let age: Int
}
class NonSendableClass {
var counter: Int = 0
}
W przykładzie UserProfile może być bezpiecznie przesłany między wątkami, ponieważ jest to struktura ze stałymi właściwościami typów Sendable. NonSendableClass spowoduje błąd kompilacji przy próbie przesłania przez await lub do Task.
Typy Sendable w Swift dzielą się na trzy kategorie: typy wartościowe z właściwościami Sendable, finalne klasy z niezmiennym stanem oraz funkcje/zamykania oznaczone @Sendable. Kompilator automatycznie wyprowadza zgodność z Sendable dla typów wartościowych.
Struct, enum i tuple automatycznie stają się Sendable, jeśli wszystkie ich właściwości i wartości stowarzyszone również są Sendable. To konserwatywne podejście: jeśli choć jedna właściwość nie jest zgodna z Sendable, cały typ nie zostanie uznany za bezpieczny.
| Typ | Sendable | Warunek |
|---|---|---|
| Struct | Tak (bez jawnego wskazania) | Wszystkie właściwości Sendable |
| Enum | Tak (bez jawnego wskazania) | Wszystkie associated values Sendable |
| Final class | Tak (z jawnym wskazaniem) | Wszystkie let-właściwości Sendable, brak var |
| Non-final class | Nie | Nie może być Sendable z powodu dziedziczenia |
Wszystkie wbudowane typy Swift — Int, String, Double, Bool, Optional, Array, Dictionary, Set — są zgodne z Sendable. To sprawia, że kompozycja typów jest domyślnie bezpieczna. Deweloper musi jedynie uważać na niestandardowe klasy.
@unchecked Sendable — to mechanizm pozwalający klasie jawnie zadeklarować się jako Sendable, z pominięciem sprawdzania przez kompilator. Deweloper bierze na siebie odpowiedzialność za bezpieczeństwo wątkowe takiej klasy. Jest to przydatne dla mostów Objective-C i zoptymalizowanych struktur.
@unchecked Sendable stosuje się, gdy klasa wewnętrznie gwarantuje bezpieczeństwo poprzez blokady lub operacje atomowe, ale kompilator nie może tego statycznie sprawdzić. Na przykład klasa z os_unfair_lock lub pthread_mutex_t — jej bezpieczeństwo wątkowe jest zapewnione przez kod, ale Swift tego nie widzi.
final class AtomicCounter: @unchecked Sendable {
private var value: Int = 0
private let lock = NSLock()
func increment() {
lock.lock()
value += 1
lock.unlock()
}
}
Należy używać @unchecked Sendable ostrożnie. To wyraźny sygnał dla innych deweloperów: „sprawdziłem, to jest bezpieczne”. Błąd w implementacji może prowadzić do trudnych do wykrycia wyścigów. Przed zastosowaniem @unchecked warto upewnić się, że typ naprawdę nie może być przepisany jako typ wartościowy.
Actor i Sendable to dwie strony tego samego medalu. Actor izoluje swój stan, ale do wymiany danych ze światem zewnętrznym musi zwracać typy Sendable. Jeśli metoda actor zwraca typ nie-Sendable, kompilator zgłasza ostrzeżenie lub błąd.
Gdy actor wysyła dane do kodu zewnętrznego, dane te przekraczają granicę izolacji. Sendable gwarantuje, że odbiorca może bezpiecznie używać ich poza aktorem. Sam actor pozostaje izolowany — jego wewnętrzny stan nie jest ujawniany.
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 — to struktura Sendable, która zawiera tylko let-właściwości typów Sendable. Takie podejście to best practice przy wyodrębnianiu danych z actor. Migawka stanu jest przesyłana przez wartość, a actor nie traci kontroli nad swoim stanem.
@Sendable — to atrybut dla funkcji i zamykań, który gwarantuje, że zamykanie nie przechwytuje nie-Sendable danych w zmienialny sposób. Gdy zamykanie jest przesyłane do Task lub metody actor, musi być Sendable.
Kompilator sprawdza, czy @Sendable zamykanie nie przechwytuje zmienialnych referencji do klas. Przechwytywanie let-właściwości typów Sendable jest dozwolone. Przechwytywanie var-zmiennej typu referencyjnego spowoduje błąd, ponieważ zamykanie może być wykonane jednocześnie z mutacją.
func performAsync(operation: @Sendable () -> Void) {
Task {
await operation()
}
}
let constant = "Safe"
var counter = 0
// counter — przechwytywanie var mutuje licznik, błąd kompilacji
performAsync { // ❌ Mutacja przechwyconej var
print(constant)
}
Zasada jest prosta: @Sendable zamykanie może przechwytywać tylko te dane, które same są Sendable i nie będą mutowane z zewnątrz. Dla klas dozwolone jest przechwytywanie słabej referencji weak self, jeśli klasa nie jest oznaczona jako Sendable. Zapobiega to klasycznym cyklom retain i wyścigom.
Często zadawane pytania
Sendable — to znacznik, który mówi kompilatorowi: „ten typ można bezpiecznie przesyłać między wątkami”. Struktury i stałe są zwykle Sendable domyślnie, klasy — nie, chyba że zostało to jawnie określone.
Typy wartościowe (struct, enum) z właściwościami Sendable, finalne klasy z niezmiennym stanem, a także wszystkie podstawowe typy Swift: Int, String, Double, Bool, Array, Dictionary, Optional — są automatycznie zgodne z Sendable.
@unchecked Sendable — to sposób na zadeklarowanie klasy jako Sendable bez statycznego sprawdzania przez kompilator. Deweloper gwarantuje bezpieczeństwo samodzielnie, często poprzez blokady lub operacje atomowe.
Actor izoluje swój stan, ale przy zwracaniu danych do kodu zewnętrznego dane te przekraczają granicę izolacji. Sendable gwarantuje, że odbiorca nie napotka wyścigów przy używaniu tych danych poza aktorem.
Dodaj protokół Sendable do final-klasy, której wszystkie właściwości są stałymi typów Sendable. Jeśli klasa używa blokad, można zastosować @unchecked Sendable, ale wymaga to ostrożności.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również