I've been trying out the new SWIFT_STRICT_CONCURRENCY flag in Xcode 14 and I've come across the following scenario that I am unsure on how to best resolve. Here's a simplified example:
private var xKey = false
protocol Foo: AnyObject {
var x: String { get }
}
extension Foo {
var x: String {
if let x = objc_getAssociatedObject(self, &xKey) as? String {
return x
} else {
let x = "<more interesting in real life>"
objc_setAssociatedObject(self, &xKey, x, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return x
}
}
}
At the point of use of &xKey I am getting the following warning:
Reference to var 'xKey' is not concurrency-safe because it involves shared mutable state
What would be the best way to resolve this? Is there any annotation I can use to pinky-swear to the compiler that this is OK?
The only solution I've found so far, which feels heavy, is to define my key like this instead:
private final class Key: @unchecked Sendable {
var value = false
}
private let xKey = Key()
Got it. Yes, that does resolve the issue, thanks for the help!
I do wonder if this could be generalized to a different case that triggers the same warning though:
I am using the "environment/world" pattern described by the pointfree folks.
struct World: @unchecked Sendable {
var date: () -> Date
init() {
date = Date.init
}
}
var Current = World()
// elsewhere
func foo() {
print(Current.date()) // Reference to var 'Current' is not concurrency-safe because it involves shared mutable state
}
Current is a var, but it is effectively a let once configured in app-launch so it is safe, but I'm unsure of how to convince the compiler of that. I've been hoping there was some sort of @unchecked-ish annotation to use for vars.
struct World: @unchecked Sendable {
var date: () -> Date
init() {
date = Date.init
}
}
class Current {
static let current = Current()
var world: World
private init() {
// this init will be called on the first Current.current call
// configure the world appropriately here
world = World()
}
}
// elsewhere
func foo() {
print(Current.current.world.date())
}
I've come up with this which gives me the same interface as before, like being able to just call Current.date(). Still really wish there was some kind of annotation though to just mark a var is unchecked .
struct World: @unchecked Sendable {
var date: () -> Date
init() {
date = Date.init
}
}
@dynamicMemberLookup
final class WorldContainer<W: Sendable>: @unchecked Sendable {
var world: W
init(_ world: W) {
self.world = world
}
subscript<V>(dynamicMember keyPath: KeyPath<W, V>) -> V {
world[keyPath: keyPath]
}
subscript<V>(dynamicMember keyPath: WritableKeyPath<W, V>) -> V {
get {
world[keyPath: keyPath]
}
set {
world[keyPath: keyPath] = newValue
}
}
}
let Current = WorldContainer(World())
func foo() {
print(Current.date())
}
The pattern is used as a DI solution so it needs to remain a var so that you configure it differently while in unit tests. (Explained much better over here.)