5. Sendable
5. Sendable
Teori: Keamanan Data Lintas Batas Actor
Sendable adalah protokol marker yang menandai bahwa sebuah tipe aman untuk dikirim
lintas batas concurrency (dari satu actor ke actor lain, atau ke Task baru).
Actor A ──► [UserDTO: Sendable] ──► Actor B ✅ Aman
Actor A ──► [UserObject: class] ──► Actor B ⚠️ Berbahaya — bisa data race
Apa itu "Melewati Batas Actor"?
Batas actor dilewati setiap kali nilai dikirim antar isolation domain yang berbeda —
ditandai dengan await saat memanggil konteks lain.
┌─────────────────────┐ ┌─────────────────────┐
│ actor Interactor │ │ @MainActor │
│ │──────▶│ Presenter │
│ isolation domain A │ kirim │ isolation domain B │
└─────────────────────┘ data └─────────────────────┘
▲
INI yang disebut
"melewati batas actor"
Struct menjadi tidak Sendable ketika memiliki properti bertipe class yang bukan Sendable:
// Class biasa — bukan Sendable
class ImageFilter {
var intensity: Double = 1.0
var name: String = "vivid"
}
// Struct ini TIDAK implicitly Sendable
// karena ImageFilter (class) bukan Sendable
struct PhotoRequest {
let userId: String
let filter: ImageFilter // ← class reference, bukan Sendable
}
Tiga skenario melewati batas actor:
// Skenario 1 — dari actor ke @MainActor (via presenter)
actor PhotoInteractor {
weak var presenter: PresenterLogic?
func upload() async {
let request = PhotoRequest(userId: "ari", filter: ImageFilter())
// ⚠️ Swift 5.x: warning | ❌ Swift 6: error
// PhotoRequest melewati batas dari actor → @MainActor
await presenter?.present(request: request)
}
}
// Skenario 2 — dari actor ke actor lain
actor PhotoInteractor {
let storage = PhotoStorage() // actor lain
func process() async {
let request = PhotoRequest(userId: "ari", filter: ImageFilter())
// ⚠️ Swift 5.x: warning | ❌ Swift 6: error
// PhotoRequest melewati batas dari PhotoInteractor → PhotoStorage
await storage.save(request)
}
}
// Skenario 3 — capture di dalam Task.detached
@MainActor
class ViewController: UIViewController {
func upload() {
let request = PhotoRequest(userId: "ari", filter: ImageFilter())
Task.detached {
// ⚠️ Swift 5.x: warning | ❌ Swift 6: error
// capture PhotoRequest dari @MainActor ke Task.detached (no isolation)
await uploadService.send(request)
}
}
}
Yang TIDAK dianggap melewati batas — dipakai di dalam isolation domain yang sama:
actor PhotoInteractor {
func process() async {
let request = PhotoRequest(userId: "ari", filter: ImageFilter())
// ✅ Tidak error — dipakai di dalam actor yang sama
print(request.userId)
validate(request)
}
func validate(_ r: PhotoRequest) { ... } // masih di actor ini
}
Solusi:
// Opsi 1 — ubah class jadi struct (paling direkomendasikan)
struct ImageFilter: Sendable {
var intensity: Double = 1.0
var name: String = "vivid"
}
struct PhotoRequest: Sendable {
let userId: String
let filter: ImageFilter // ✅ struct Sendable
}
// Opsi 2 — jika class tidak bisa diubah, final + semua properti immutable
final class ImageFilter: @unchecked Sendable {
let intensity: Double // let — tidak bisa diubah setelah init
let name: String
init(intensity: Double, name: String) {
self.intensity = intensity
self.name = name
}
}
Sendable Otomatis
// Struct dengan semua properti Sendable → otomatis Sendable
struct UserDTO: Sendable {
let id: UUID // UUID: Sendable ✅
let name: String // String: Sendable ✅
let age: Int // Int: Sendable ✅
}
// Enum dengan associated values yang Sendable → otomatis Sendable
enum LoadingState: Sendable {
case idle
case loading
case success(UserDTO) // UserDTO: Sendable ✅
case failure(String) // String: Sendable ✅
}
// Value type sederhana tidak perlu deklarasi eksplisit (compiler inferkan)
@unchecked Sendable — Opt-out Safety Check
Untuk class yang thread-safe secara manual tapi tidak bisa diverifikasi compiler:
// NSCache tidak conform Sendable, tapi thread-safe
final class ImageCacheWrapper: @unchecked Sendable {
// NSCache sudah thread-safe secara internal
private let cache = NSCache<NSString, UIImage>()
func set(_ image: UIImage, for key: String) {
cache.setObject(image, forKey: key as NSString)
}
func get(_ key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
}
// Class dengan lock manual
final class ThreadSafeCounter: @unchecked Sendable {
private var _value: Int = 0
private let lock = NSLock()
var value: Int {
lock.withLock { _value }
}
func increment() {
lock.withLock { _value += 1 }
}
}
Perhatian:
@unchecked Sendableartinya kamu yang bertanggung jawab membuktikan thread safety-nya. Jangan gunakan sebagai jalan pintas untuk menyembunyikan bug.
@Sendable Closure
Closure yang akan dikirim ke Task atau actor lain harus ditandai @Sendable:
// @Sendable closure — tidak boleh capture mutable non-Sendable state
func performAsync(work: @Sendable @escaping () async -> Void) {
Task {
await work()
}
}
// Penggunaan
performAsync {
// Boleh: immutable capture
let localValue = someImmutableValue
await process(localValue)
}
// Salah — compiler warning di Swift 5.x (error di Swift 6)
var mutableState = [String]()
performAsync {
mutableState.append("item") // ⚠️ Mutation dari @Sendable closure
}