3.9 KiB
Unable to conform to Sendable with a non-Sendable superclass (NonSendableSuperclass)
Overview
A class cannot conform to Sendable when it inherits from a non-Sendable superclass. This prohibition exists because the superclass is free to change its exposed API or internal implementation in ways that would make a Sendable conformance unsafe. Consider code like this:
class MIDIDevice {}
// ...
final class MIDISynth: MIDIDevice, Sendable {}
// | `- note: inherits from non-Sendable class 'MIDIDevice'
// |- error: class 'MIDISynth' cannot conform to the 'Sendable' protocol
// `- note: a 'Sendable' class cannot inherit from a non-Sendable class
Since MIDIDevice isn't Sendable, it is allowed to expose mutable state:
class MIDIDevice {
var activeNotes: [Note] = []
}
MIDISynth would inherit activeNotes, and treating MIDISynth as Sendable would allow activeNotes to be mutated concurrently, risking a data race.
Normally, a global actor like @MainActor implies Sendable, but this is not the case for classes with non-Sendable superclasses for the same reason.
@MainActor
class MIDISynth: MIDIDevice {} // Not implicitly Sendable!
If @MainActor made MIDISynth conform to Sendable, MIDISynth could be sent to multiple actors, upcast to MIDIDevice, and concurrently mutated:
@MainActor
func useSynth(_ synth: MIDISynth) {
Task.detached { // sending would only be possible if MIDISynth were Sendable
let device: MIDIDevice = synth // upcast strips the @MainActor isolation
device.activeNotes.append(.A) // mutates off the main actor...
}
synth.activeNotes.append(.C) // ...racing with the detached task's mutation
}
However, since MIDISynth is not allowed to conform to Sendable, sending it to a task while still referencing it on the main actor will be an error.
How to fix
If you need to send a class that inherits from a non-Sendable superclass, you have a few options:
-
If you wrote the superclass, isolate the superclass to a global actor. Its state is then actor-protected, the base becomes implicitly
Sendable, and the subclass inherits that conformance:@MainActor class MIDIDevice { var activeNotes: [Note] = [] } class MIDISynth: MIDIDevice {} // inherits Sendable and @MainActor from MIDIDevice -
If you don't control the superclass, you may conform with
@unchecked Sendableif you guarantee the safety of the inherited and subclass state yourself:class MIDISynth: MIDIDevice, @unchecked Sendable {} -
This option also applies to a global-actor-isolated subclass, but be aware that your subclass can be upcast after being sent, so actor isolation is insufficient to protect the inherited state:
@MainActor class MIDISynth: MIDIDevice, @unchecked Sendable {} -
Instead of trying to conform to
Sendable, store the instance of the class that you need to share inside a synchronized container, likeMutex:import Synchronization let synth = Mutex(MIDISynth()) Task.detached { synth.withLock { $0.activeNotes.append(.A) } }
NSObject
An exception is made for directly inheriting from NSObject. NSObject does not and will not expose mutable stored properties, so direct subclasses are allowed to conform to Sendable for Objective-C interoperability.