| Aspect | Struct | Class | Actor |
|---|---|---|---|
| Type semantics | Value type (copied on assignment) | Reference type (shared instance) | Reference type (shared instance) |
| Inheritance | ❌ No | ✅ Single inheritance | ❌ No (only NSObject for ObjC interop) |
| Protocol conformance | ✅ Yes | ✅ Yes | ✅ Yes, but sync protocol requirements need nonisolated or isolated conformance |
Identity (===) |
❌ No | ✅ Yes | ✅ Yes |
| Mutating methods | Need the mutating keyword |
Not needed | Not needed, since mutation is isolated |
let instance behavior |
Fully immutable | Properties can still be mutated | Properties mutable only from inside the actor |
| Memberwise init | ✅ Auto-generated | ❌ Must write init |
❌ Must write init |
deinit |
❌ No (except ~Copyable structs) |
✅ Yes | ✅ Yes (isolated deinit in Swift 6.2+) |
| Memory | Usually stack/inline (heap if boxed, escaping, or existential) | Heap, ARC-managed | Heap, ARC-managed |
| Copy-on-write | Built-in for stdlib collections, manual for custom | N/A | N/A |
| Thread safety | Safe by copy, since each thread gets its own copy | ❌ Not safe, needs locks or queues manually | ✅ Safe, with compiler-enforced data isolation |
Sendable |
Implicit if all stored properties are Sendable (non-public types) |
Only if final with immutable Sendable props, otherwise @unchecked Sendable |
✅ Always implicitly Sendable |
| External access | Synchronous | Synchronous | await required from outside the actor (async hop) |
| Reentrancy | N/A | N/A | ⚠️ Reentrant at every await, so state can change across suspension points |
nonisolated members |
N/A | N/A | ✅ For sync access to immutable or Sendable state |
| Global actor support | Can be annotated (@MainActor struct) |
Can be annotated (@MainActor class) |
Is itself an actor; @globalActor defines shared ones |
| ObjC interop | ❌ No | ✅ Via NSObject / @objc |
Limited |
| Performance | Fastest, with no ARC or indirection for simple types | ARC overhead, dynamic dispatch unless final |
ARC plus executor hop cost for each await |
| Typical use | Models, DTOs, config, value data | Shared mutable state, UIKit, delegates, ObjC APIs | Shared mutable state across concurrent tasks |
final, and use a lock for thread safety.final classThis choice is relevant to your StateBroadcaster work. An actor forces await on every call and brings reentrancy concerns. A final class with OSAllocatedUnfairLock marked @unchecked Sendable gives you synchronous access. That is often better inside NEPacketTunnelProvider callbacks, or when sync protocol requirements must be satisfied. The trade-off is that correctness is then your responsibility instead of the compiler’s.