Assignment vs mutation and thread safety

So in swift6 this code I was using is no longer allowed:

@Observable
class ChatViewModel {
    var messages: [Message] = []

    func loadMessages() {
        sdk.loadMessagesInBackground() { messages in
            Task { @MainActor in
                self.messages = messages
            }
        }
    }
}

The issue being that I cannot move "self" to MainActor. I could declare @MainActor on the whole class but I was trying to see what else might be possible. I did this and it worked:

@Observable
class ChatViewModel {
    var messages: [Message] = []

    func loadMessages() {
        sdk.loadMessagesInBackground() { messages in
            var selfMessages = self.messages
            Task { @MainActor in
                selfMessages.replaceSubrange(0..<selfMessages.count, with: messages)
            }
        }
    }
}

So I'm just curious now how these are different. The actual thread safety issues should be identical between the two of them, but one is allowed. Why?

These snippets are not identical in their semantics at all. var selfMessages = self.messages will create a copy of self.messages, not a reference. When modifying selfMessages, you're modifying that copy, and nothing else has access to it other than the unstructured task. Because of that, loadMessages() doesn't modify self.messages and there's no mutation of self.messages whatsosever, which I'd assume is a bug in your case.

5 Likes