Rules about sync-async behavior on default initializers when class is MainActor?

I'm not blocked on these… but I was looking for more documentation or advice to help understand these rules and conventions about when await should be used on constructors.

@MainActor class C1 {
  init() {
    
  }
}

@MainActor func f1() async {
  let _ = C1()
}

func f2() async {
  let _ = await C1()
}

@MainActor class C2 {
  
}

@MainActor func f3() async {
  let _ = C2()
}

func f4() async {
  let _ = await C2()
          `- warning: no 'async' operations occur within 'await' expression
}

The C1 class is MainActor and defines an init without async.

The f1 function is MainActor. I construct a C1 instance without await. I understand this.

The f2 function is not MainActor. I construct a C1 instance with await. I understand this.

The C2 class is MainActor and does not define an init.

The f3 function is MainActor. I construct a C2 instance without await. I understand this.

The f4 function is not MainActor. I construct a C2 instance with await and see a warning. I do not understand why this warning happened.

The init explicitly defined for C1 is not defined as async. I still need to await when I come from a context that might not be MainActor: the f2 function.

The default init implicitly defined for C2 seems to be "more sync than sync"? Because I can come from a context that might not be MainActor and construct without await: the f4 function.

There might be some clues here in [SE-0327][1]. Would the answers I was looking for be in there? I didn't find a mention in there of how those rules are meant to affect default initializers on classes.

Any more advice about that? Thanks!


  1. swift-evolution/proposals/0327-actor-initializers.md at main · swiftlang/swift-evolution · GitHub ↩︎

When a type is attributed with a global actor, by default, all its members, including initializers are isolated to that actor. So you need to call them using await when the caller does not share the same isolation. If you do not want certain synchronous members to be called that way, you can explicitly mark them nonisolated, including initializers. Apparently, for the default initializer of C2, I guess Swift automatically treat it as nonisolated (just for convenience, maybe).

I believe the behaviors of your example are natural outcomes of SE-0316.

SE-0327 do focuses on initialization, but I think that proposal is about the internal behavior inside initializers themselves, and that should not affect how callers can invoke them.

2 Likes

Hmm… what about this?

@MainActor class C3 {
  let c1: C1
  nonisolated init() {
    self.c1 = C1()
              `- error: call to main actor-isolated initializer 'init()' in a synchronous nonisolated context
  }
}

@MainActor class C4 {
  let c1: C1 = C1()
}

If the C4 default initializer was nonisolated… would that not mean I should be blocked on constructing the C1 instance as a default value?

Ahh… wait:

func f5() async {
  let _ = await C4()
}

No warning here… so the default initializer is not nonisolated?

After some more digging, I realized this is the formal rule.

As for your additional code, the synthesized initializer for C4 is main actor isolated according to the rule.

3 Likes

Ahh… there it is! Thanks!

1 Like