Singletons usually talking get a foul rep. Folks don’t like them, they trigger points, and customarily talking it’s simply not nice apply to depend on globally accessible mutable state in your apps. As an alternative, it’s extra favorable to apply specific dependency passing which makes your code extra testable and dependable total.
That stated, typically you’ll have singletons. Or, extra possible, you’ll wish to have a a shared occasion of one thing that you just want in a handful of locations in your app:
class AuthProvider {
static let shared = AuthProvider()
// ...
}
In Swift 6, this may result in points as a result of Swift 6 doesn’t like non-Sendable varieties, and it additionally doesn’t like world mutable state.
On this submit, you’ll be taught concerning the causes that Swift 6 will flag your singletons and shared cases as problematic, and we’ll see what you are able to do to fulfill the Swift 6 compiler. We’ll run by a number of completely different errors which you can get in your shared cases relying on the way you’ve structured your code.
Static property ‘shared’ just isn’t concurrency-safe as a result of it’s nonisolated world shared mutable state
We’ll begin off with an error that you just’ll get for any static property that’s mutable no matter whether or not this property is used for a shared occasion or not.
For instance:
class AuthProvider {
// Static property 'shared' just isn't concurrency-safe as a result of it
// is nonisolated world shared mutable state
static var shared = AuthProvider()
non-public init() {}
}
class GamePiece {
// Static property 'energy' just isn't concurrency-safe as a result of it
// is nonisolated world shared mutable state
static var energy = 100
}
As you’ll be able to see, each GamePiece
and AuthProvider
get the very same error. They’re not concurrency-safe as a result of they’re not remoted they usually’re mutable. Meaning we would mutate this static let from a number of duties and that will result in information races (and crashes).
To resolve this error, we are able to take completely different approaches relying on the utilization of our static var
. If we actually want our static member to be mutable, we should always be sure that we are able to safely mutate and which means we have to isolate our mutable state by some means.
Resolving the error when our static var must be mutable
We’ll begin off by taking a look at our GamePiece
; it actually wants energy
to be mutable as a result of we are able to improve its worth all through the imaginary sport I take into account.
Isolating GamePiece to the primary actor
One method is to isolate our GamePiece
or static var energy
to the primary actor:
// we are able to isolate our GamePiece to the primary actor
@MainActor
class GamePiece {
static var energy = 100
}
// or we isolate the static var to the primary actor
class GamePiece {
@MainActor
static var energy = 100
}
The primary possibility is smart when GamePiece is a category that’s designed to intently work with our UI layer. Once we solely ever work with GamePiece from the UI, it is smart to isolate your complete object to the primary actor. This simplifies our code and makes it in order that we’re not going from the primary actor’s isolation to another isolation and again on a regular basis.
Alternatively, if we don’t need or want your complete GamePiece
to be remoted to the primary actor we are able to additionally select to solely isolate our static var
to the primary actor. Because of this we’re studying and writing energy
from the primary actor always, however we are able to work with different strategies an properties on GamePiece
from different isolation contexts too. This method usually results in extra concurrency in your app, and it’ll make your code extra complicated total.
There’s a second possibility that we are able to attain for, nevertheless it’s one which it is best to solely use if constraining your sort to a worldwide actor is unnecessary.
It’s nonisolated(unsafe)
.
Permitting static var with nonisolated(unsafe)
Typically you’ll know that your code is protected. For instance, you would possibly know that energy
is just accessed from a single process at a time, however you don’t wish to encode this into the sort by making the property major actor remoted. This is smart as a result of perhaps you’re not accessing it from the primary actor however you’re utilizing a worldwide dispatch queue or a indifferent process.
In these sorts of conditions the one actual appropriate answer can be to make GamePiece
an actor. However that is usually non-trivial, introduces quite a lot of concurrency, and total makes issues extra complicated. Once you’re engaged on a brand new codebase, the implications wouldn’t be too unhealthy and your code can be extra “appropriate” total.
In an present app, you often wish to be very cautious about introducing new actors. And if constraining to the primary actor isn’t an possibility you would possibly want an escape hatch that tells the compiler “I do know you don’t like this, nevertheless it’s okay. Belief me.”. That escape hatch is nonisolated(unsafe)
:
class GamePiece {
nonisolated(unsafe) static var energy = 100
}
Once you mark a static var
as nonisolated(unsafe)
the compiler will not carry out data-race safety checks for that property and also you’re free to make use of it nonetheless you please.
When issues are working nicely, that’s nice. Nevertheless it’s additionally dangerous; you’re now taking up the guide accountability of forestall information races. And that’s a disgrace as a result of Swift 6 goals to assist us catch potential information races at compile time!
So use nonisolated(unsafe)
sparingly, mindfully, and attempt to do away with it as quickly as doable in favor of isolating your world mutable state to an actor.
Notice that in Swift 6.1 you could possibly make GamePiece
an actor and the Swift compiler will permit you to have static var energy = 100
with out points. It is a bug within the compiler and nonetheless counts as a possible information race. A repair has already been merged to Swift’s major department so I’d anticipate that Swift 6.2 emits an acceptable error for having a static var
on an actor.
Resolving the error for shared cases
Once you’re working with a shared occasion, you sometimes don’t want the static var
to be a var
in any respect. When that’s the case, you’ll be able to really resolve the unique error fairly simply:
class AuthProvider {
static let shared = AuthProvider()
non-public init() {}
}
Make the property a let
as a substitute of a var
and Static property 'shared' just isn't concurrency-safe as a result of it's nonisolated world shared mutable state
goes away.
A brand new error will seem although…
Static property ‘shared’ just isn’t concurrency-safe as a result of non-‘Sendable’ sort ‘AuthProvider’ might have shared mutable state
Let’s dig into that error subsequent.
Static property ‘shared’ just isn’t concurrency-safe as a result of non-‘Sendable’ sort might have shared mutable state
Whereas the brand new error sounds so much just like the one we had earlier than, it’s fairly completely different. The primary error complained that the static var
itself wasn’t concurrency-safe, this new error isn’t complaining concerning the static let
itself. It’s complaining that we have now a globally accessible occasion of our sort (AuthProvider
) which could not be protected to work together with from a number of duties.
If a number of duties try and learn or mutate state on our occasion of AuthProvider
, each process would work together with the very same occasion. So if AuthProvider
can’t deal with that accurately, we’re in hassle.
The way in which to repair this, is to make AuthProvider
a Sendable
sort. In the event you’re undecided that you just absolutely perceive Sendable simply but, be certain to learn this submit about Sendable so that you’re caught up.
The brief model of Sendable
is {that a} Sendable
sort is a sort that’s protected to work together with from a number of isolation contexts.
Making AuthProvider Sendable
For reference varieties like our AuthProvider
being Sendable
would imply that:
AuthProvider
can’t have any mutable state
- All members of
AuthProvider
should even be Sendable
AuthProvider
should be a closing class
- We manually conform
AuthProvider
to the Sendable
protocol
Within the pattern code, AuthProvider
didn’t have any state in any respect. So if we’d repair the error for our pattern, I’d be capable to do the next:
closing class AuthProvider: Sendable {
static let shared = AuthProvider()
non-public init() {}
}
By making AuthProvider
a Sendable
sort, the compiler will permit us to have a shared occasion with none points as a result of the compiler is aware of that AuthProvider
can safely be used from a number of isolation contexts.
However what if we add some mutable state to our AuthProvider
?
closing class AuthProvider: Sendable {
static let shared = AuthProvider()
// Saved property 'currentToken' of
// 'Sendable'-conforming class 'AuthProvider' is mutable
non-public var currentToken: String?
non-public init() {}
}
The compiler doesn’t permit our Sendable
sort to have mutable state. It doesn’t matter that this state is non-public
, it’s merely not allowed.
Utilizing nonisolated(unsafe) as an escape hatch once more
If we have now a shared occasion with mutable state, we have now a number of choices out there to us. We may take away the Sendable
conformance and make our static let
a nonisolated(unsafe)
property:
class AuthProvider {
nonisolated(unsafe) static let shared = AuthProvider()
non-public var currentToken: String?
non-public init() {}
}
This works nevertheless it’s most likely the worst possibility we have now as a result of it doesn’t defend our mutable state from information races.
Leveraging a worldwide actor to make AuthProvider Sendable
Alternatively, we may apply isolate our sort to the primary actor similar to we did with our static var
:
// we are able to isolate our class
@MainActor
class AuthProvider {
static let shared = AuthProvider()
non-public var currentToken: String?
non-public init() {}
}
// or simply the shared occasion
class AuthProvider {
@MainActor
static let shared = AuthProvider()
non-public var currentToken: String?
non-public init() {}
}
The professionals and cons of this options are the identical as they had been for the static var
. If we largely use AuthProvider
from the primary actor that is fantastic, but when we ceaselessly have to work with AuthProvider
from different isolation contexts it turns into a little bit of a ache.
Making AuthProvider an actor
My most popular answer is to both make AuthProvider
conform to Sendable
like I confirmed earlier, or to make AuthProvider
into an actor:
actor AuthProvider {
static let shared = AuthProvider()
non-public var currentToken: String?
non-public init() {}
}
Actors in Swift are all the time Sendable
which implies that an actor can all the time be used as a static let
.
There’s yet one more escape hatch…
Let’s say we are able to’t make AuthProvider
an actor as a result of we’re working with present code and we’re not able to pay the value of introducing a great deal of actor-related concurrency into our codebase.
Perhaps you’ve had AuthProvider
in your mission for some time and also you’ve taken acceptable measures to make sure its concurrency-safe.
If that’s the case, @unchecked Sendable
can assist you bridge the hole.
Utilizing @unchecked Sendable as an escape hatch
Marking our class as @unchecked Sendable
will be finished as follows:
closing class AuthProvider: @unchecked Sendable {
static let shared = AuthProvider()
non-public var currentToken: String?
non-public init() {}
}
An escape hatch like this ought to be used rigorously and will ideally be thought of a short lived repair. The compiler received’t complain however you’re open to data-races that the compiler can assist forestall altogether; it’s like a sendability force-unwrap.
In Abstract
Swift 6 permits singletons, there’s little doubt about that. It does, nonetheless, impose fairly strict guidelines on the way you outline them, and Swift 6 requires you to be sure that your singletons and shared cases are protected to make use of from a number of duties (isolation contexts) on the similar time.
On this submit, you’ve seen a number of methods to do away with two shared occasion associated errors.
First, you noticed how one can have static var
members in a method that’s concurrency-safe by leveraging actor isolation.
Subsequent, you noticed that static let
is one other solution to have a concurrency-safe static member so long as the kind of your static let
is concurrency-safe. That is what you’ll sometimes use in your shared cases.
I hope this submit has helped you grasp static members and Swift 6 a bit higher, and that you just’re now capable of leverage actor isolation the place wanted to accurately have world state in your apps.