Default actor isolation and deinit

Consider this example:

class Base {
}

class Sub: Base {
  deinit {
  }
}

This produces an error when compiled with -default-isolation MainActor:

error: nonisolated deinitializer 'deinit' has different actor isolation from main actor-isolated overridden declaration

My understanding based on SE-0466 is that the deinit should be @MainActor-isolated by default, yet it requires annotating the deinit manually with @MainActor to silence the error.

Alternatively, it could be that deinitalisers should be nonisolated anyway (and there’s no error if I give Base a deinit, nonisolated or no annotation), but why does the isolation of Base's implicit deinit not match that of Sub's explicit one?

1 Like

Good question. SE-0371: Isolated synchronous deinit seems relevant here since it introduced the possibility for deinitializers to have any other isolation than nonisolated. But SE-0371 says that implicit deinits are always nonisolated:

Implicit deinitializers cannot opt-in into isolation, so they are nonisolated by default.

From your observations, it looks like SE-0466: Control default actor isolation inference changes this rule to "the implicit deinit in a MainActor-by-default class is implicitly isolated", but I couldn't find this being explicitly stated in the proposal.

SE-0466 gives this example showing that a class deinit in a MainActor-by-default context will be implicitly isolated:

// @MainActor
class C {
  …
  // @MainActor
  deinit { ... }
  …
}

It's not 100% clear from the proposal text whether this implicit isolation for deinit is only intended for explicit deinit definitions or also for implicit deinits, but either interpretation would seem to contradict your observations (implicit deinit is isolated, explicit deinit in subclass is nonisolated).


According to SE-0371, annotating the deinit with @MainActor isn't wrong, but the recommended syntax is to use isolated deinit { … }.


Are you compiling your code in the Swift 6 language mode? I'm asking because when compiling your code with -language-mode 6, there is another compiler error related to init() in the subclass. This error seems vaguely related, but I also don't understand it:

swiftc -language-mode 6 -default-isolation MainActor test.swift
[SNIP error about deinit]

test.swift:4:17: error: main actor-isolated initializer 'init()' has different actor isolation from nonisolated overridden declaration
1 | class Base {
  |       `- note: overridden declaration is here
2 | }
3 |
4 | class Sub: Base {
  |                 `- error: main actor-isolated initializer 'init()' has different actor isolation from nonisolated overridden declaration
5 |   deinit {
6 |   }

My takeaway is that I don't understand the exact rules how MainActor-by-default applies to subclasses.

1 Like

From my understanding of SE-371, explicit deinit's require to be marked with the isolated keyword or the global actor attribute of the base class.

SE-371 states the following:

Classes can add isolation to the non-isolated deinit of the base class, but they cannot change (remove or change actor) existing isolation. If base class has isolated deinit, all derived classes must have deinit with the same isolation.

Synthesized deinit inherits isolation of the superclass deinit automatically, but explicit deinit needs to marked with isolated or global actor attribute.

@MainActor
class Base {
  isolated deinit {}
}

class Derived: Base {
  // ok, isolation matches
  isolated deinit {}
}

class Removed: Base {
  // error: nonisolated deinitializer 'deinit' has different actor isolation from global actor 'MainActor'-isolated overridden declaration
  deinit {}
}

class Changed: Base {
  // error: global actor 'AnotherActor'-isolated deinitializer 'deinit' has different actor isolation from global actor 'MainActor'-isolated overridden declaration
  @AnotherActor deinit {}
}

class Implicit: Base {
  // ok, implicit deinit inherits isolation automatically
}

However, SE-371 also states that deint is nonisolated by default.

For consistency, nonisolated attribute also can be used, but for synchronous deinit it has no effect, because deinitializers are nonisolated by default.

@MainActor
class Foo {
  // Same as no attributes
  nonisolated deinit {}
}

It seems that -default-isolation MainActor does more than just implicitly apply the @MainActor attribute to a type declaration, it applies it to all declarations (which makes sense, I guess). Except, for some reason, it doesn’t apply to implicit initializers or deinitializers (is this an implementation oversight, perhaps?).

1 Like

Are you compiling your code in the Swift 6 language mode?

I discovered the issue in an Xcode project which still uses Swift 5 language mode. I assumed the swift command used Swift 6 language mode by default, but I guess not—I didn’t notice the additional error with init, which looks related to the deinit one.

It seems the init-based error already has a bug report: init has different actor isolation from overriden - but neither exists · Issue #84644 · swiftlang/swift · GitHub

…and with the deinit being very related, I’ll just add it there.

1 Like