Singletons usually talking get a nasty rep. Folks don’t like them, they trigger points, and customarily talking it’s simply not nice follow to depend on globally accessible mutable state in your apps. As an alternative, it’s extra favorable to follow specific dependency passing which makes your code extra testable and dependable total.
That mentioned, typically you’ll have singletons. Or, extra possible, you’ll need 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 sorts, and it additionally doesn’t like world mutable state.
On this submit, you’ll study 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 way of a number of completely different errors which you could get on your shared cases relying on the way you’ve structured your code.
Static property ‘shared’ shouldn’t be 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' shouldn't be 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' shouldn't be concurrency-safe as a result of it
// is nonisolated world shared mutable state
static var energy = 100
}
As you may 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 may result in knowledge 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 meaning we have to isolate our mutable state in some way.
Resolving the error when our static var must be mutable
We’ll begin off by our GamePiece
; it actually wants energy
to be mutable as a result of we are able to improve its worth all through the imaginary recreation I take note of.
Isolating GamePiece to the principle actor
One strategy is to isolate our GamePiece
or static var energy
to the principle actor:
// we are able to isolate our GamePiece to the principle actor
@MainActor
class GamePiece {
static var energy = 100
}
// or we isolate the static var to the principle actor
class GamePiece {
@MainActor
static var energy = 100
}
The primary choice is smart when GamePiece is a category that’s designed to intently work with our UI layer. After we solely ever work with GamePiece from the UI, it is smart to isolate your complete object to the principle actor. This simplifies our code and makes it in order that we’re not going from the principle 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 principle actor we are able to additionally select to solely isolate our static var
to the principle actor. Because of this we’re studying and writing energy
from the principle actor always, however we are able to work with different strategies an properties on GamePiece
from different isolation contexts too. This strategy usually results in extra concurrency in your app, and it’ll make your code extra complicated total.
There’s a second choice that we are able to attain for, but it surely’s one which you must solely use if constraining your sort to a world actor is senseless.
It’s nonisolated(unsafe)
.
Permitting static var with nonisolated(unsafe)
Generally you’ll know that your code is secure. For instance, you would possibly know that energy
is barely accessed from a single activity at a time, however you don’t need to encode this into the kind by making the property major actor remoted. This is smart as a result of possibly you’re not accessing it from the principle actor however you’re utilizing a world dispatch queue or a indifferent activity.
In these sorts of conditions the one actual appropriate answer could be to make GamePiece
an actor. However that is typically non-trivial, introduces lots of concurrency, and total makes issues extra complicated. Whenever you’re engaged on a brand new codebase, the results wouldn’t be too dangerous and your code could be extra “appropriate” total.
In an present app, you often need to be very cautious about introducing new actors. And if constraining to the principle actor isn’t an choice you would possibly want an escape hatch that tells the compiler “I do know you don’t like this, but it surely’s okay. Belief me.”. That escape hatch is nonisolated(unsafe)
:
class GamePiece {
nonisolated(unsafe) static var energy = 100
}
Whenever 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 nevertheless you please.
When issues are working properly, that’s nice. However it’s additionally dangerous; you’re now taking up the guide duty of forestall knowledge races. And that’s a disgrace as a result of Swift 6 goals to assist us catch potential knowledge races at compile time!
So use nonisolated(unsafe)
sparingly, mindfully, and attempt to do away with it as quickly as attainable in favor of isolating your world mutable state to an actor.
Be aware that in Swift 6.1 you possibly can make
GamePiece
an actor and the Swift compiler will assist you to havestatic var energy = 100
with out points. This can be a bug within the compiler and nonetheless counts as a possible knowledge race. A repair has already been merged to Swift’s major department so I might anticipate that Swift 6.2 emits an applicable error for having astatic var
on an actor.
Resolving the error for shared cases
Whenever 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 may truly resolve the unique error fairly simply:
class AuthProvider {
static let shared = AuthProvider()
non-public init() {}
}
Make the property a let
as an alternative of a var
and Static property 'shared' shouldn't be 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’ shouldn’t be concurrency-safe as a result of non-‘Sendable’ sort ‘AuthProvider’ could have shared mutable state
Let’s dig into that error subsequent.
Static property ‘shared’ shouldn’t be concurrency-safe as a result of non-‘Sendable’ sort could 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 secure to work together with from a number of duties.
If a number of duties try to learn or mutate state on our occasion of AuthProvider
, each activity would work together with the very same occasion. So if AuthProvider
can’t deal with that accurately, we’re in bother.
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 sure that to learn this submit about Sendable so that you’re caught up.
The quick model of Sendable
is {that a} Sendable
sort is a kind that’s secure to work together with from a number of isolation contexts.
Making AuthProvider Sendable
For reference sorts 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 aremaining class
- We manually conform
AuthProvider
to theSendable
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 might be capable to do the next:
remaining class AuthProvider: Sendable {
static let shared = AuthProvider()
non-public init() {}
}
By making AuthProvider
a Sendable
sort, the compiler will enable 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
?
remaining 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 enable 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 might 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 but it surely’s most likely the worst choice we have now as a result of it doesn’t defend our mutable state from knowledge races.
Leveraging a world actor to make AuthProvider Sendable
Alternatively, we might apply isolate our sort to the principle actor identical 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 have been for the static var
. If we principally use AuthProvider
from the principle actor that is high quality, but when we steadily must work with AuthProvider
from different isolation contexts it turns into a little bit of a ache.
Making AuthProvider an actor
My most well-liked 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 at all times Sendable
which signifies that an actor can at all times be used as a static let
.
There’s another 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 applicable measures to make sure its concurrency-safe.
If that’s the case, @unchecked Sendable
may help you bridge the hole.
Utilizing @unchecked Sendable as an escape hatch
Marking our class as @unchecked Sendable
could be completed as follows:
remaining class AuthProvider: @unchecked Sendable {
static let shared = AuthProvider()
non-public var currentToken: String?
non-public init() {}
}
An escape hatch like this needs to be used fastidiously and will ideally be thought-about a brief repair. The compiler gained’t complain however you’re open to data-races that the compiler may help forestall altogether; it’s like a sendability force-unwrap.
In Abstract
Swift 6 permits singletons, there’s little question about that. It does, nevertheless, 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 secure 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 technique 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 on 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.