Files

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 Sendable if 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, like Mutex:

    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.

See Also