Cannot use subscripts to modify @MainActor Dictionaries…?

@MainActor
struct Example {
    var dict: [String: String] = [:]

    mutating func demonstrateBug() {
        Task.detached {
            await MainActor.run {
                self.dict["Oh"] = "no"
            }
        }
    }
}

That fails with the error message:

Actor-isolated property 'dict' cannot be passed 'inout' to 'async' function call

Yet if I use the equivalent - but verbose - updateValue(:forKey) method, it works fine.

Am I missing something? The above code seems correct.

Are you sure that worked? It doesn't for me:

@MainActor
struct Example {
  var dict: [String: String] = [:]

  mutating func demonstrateBug() {
    Task.detached {  // 🛑 Escaping closure captures mutating 'self' parameter
      await MainActor.run {
        self.dict.updateValue("", forKey: "")
      }
    }
  }
}

I don't think this has anything to do with @MainActor or dictionaries, but rather you cannot capture mutable variables in sendable/escaping closures. The trailing closure of Task.detached is sendable, and self is mutable since it is a mutating method.

If this kind of code were allowed it would mean value types could be mutated from outside lexical scopes, and that would not be what is expected from value types. Even in a pre-concurrency Swift this kind of code was not allowed:

struct Example {
  var dict: [String: String] = [:]

  mutating func demonstrateBug() {
    DispatchQueue.main.async { // 🛑 Escaping closure captures mutating 'self' parameter
      self.dict["a"] = "b"
    }
  }
}
2 Likes

Ah yes, I distilled my real-world case too far. In my real case it definitely works - not just compiles but works at runtime just fine - but it's a SwiftUI View and the variable in question is @State.

Equivalently, change Example to a class.

Assuming the compiler error were correct (and that updateValue(:forKey:) didn't exist), what would be the alternative way to do this?

I'm also confused as to why it thinks the Dictionary subscript is an async function call. As far as I can tell, it is not.

The problem occurs for subscripting on all standard collections, as far as I can tell.

Looks like a bug:

@MainActor final class Example {
    var dict: [String: String] = [:]

    func demonstrateBug() {
        Task.detached {
            await MainActor.run {
                self.foo() // ✅
                dict["Oh"] = "no" // 🛑 Actor-isolated property 'dict' cannot be passed 'inout' to 'async' function call
            }
        }
    }
    func foo() {
        dict["Oh"] = "no"
    }
}

Interestingly the "self." before "dict" is not even insisted upon here.

OTOH, this works fine:

    print(self.dict["Oh"])
1 Like

The diagnostic is bogus and fixed in [5.10][Concurrency] Fix a few issues with actor-isolated `inout` argument diagnostics by hborla · Pull Request #69126 · apple/swift · GitHub

4 Likes