Utilizing singletons in Swift 6 – Donny Wals


Singletons usually talking get a foul rep. Individuals don’t like them, they trigger points, and customarily talking it’s simply not nice observe to depend on globally accessible mutable state in your apps. As an alternative, it’s extra favorable to observe express dependency passing which makes your code extra testable and dependable general.

That stated, typically you’ll have singletons. Or, extra possible, you’ll need to have a a shared occasion of one thing that you simply want in a handful of locations in your app:

class AuthProvider {
  static let shared = AuthProvider()

  // ...
}

In Swift 6, it will result in points as a result of Swift 6 doesn’t like non-Sendable varieties, and it additionally doesn’t like international mutable state.

On this submit, you’ll be taught concerning the causes that Swift 6 will flag your singletons and shared situations 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 you could get to your shared situations relying on the way you’ve structured your code.

Static property ‘shared’ isn’t concurrency-safe as a result of it’s nonisolated international shared mutable state

We’ll begin off with an error that you simply’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' isn't concurrency-safe as a result of it 
  // is nonisolated international shared mutable state
  static var shared = AuthProvider()

  personal init() {}
}

class GamePiece {
  // Static property 'energy' isn't concurrency-safe as a result of it 
  // is nonisolated international shared mutable state
  static var energy = 100
}

As you possibly can 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 knowledge races (and crashes).

To resolve this error, we will 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 guarantee that we will safely mutate and meaning we have to isolate our mutable state one way or the other.

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 will improve its worth all through the imaginary recreation I keep in mind.

Isolating GamePiece to the primary actor

One method is to isolate our GamePiece or static var energy to the primary actor:

// we will 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 choice 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 all the 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 all the GamePiece to be remoted to the primary actor we will 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 will 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 advanced general.

There’s a second choice that we will attain for, however it’s one which you must solely use if constraining your sort to a worldwide actor is unnecessary.

It’s nonisolated(unsafe).

Permitting static var with nonisolated(unsafe)

Generally you’ll know that your code is protected. For instance, you may know that energy is simply accessed from a single activity at a time, however you don’t need to encode this into the kind by making the property predominant actor remoted. This is smart as a result of possibly you’re not accessing it from the primary actor however you’re utilizing a worldwide dispatch queue or a indifferent activity.

In these sorts of conditions the one actual appropriate resolution can be to make GamePiece an actor. However that is usually non-trivial, introduces a variety of concurrency, and general makes issues extra advanced. While you’re engaged on a brand new codebase, the implications wouldn’t be too unhealthy and your code can be extra “appropriate” general.

In an present app, you normally need to be very cautious about introducing new actors. And if constraining to the primary actor isn’t an choice you may want an escape hatch that tells the compiler “I do know you don’t like this, however it’s okay. Belief me.”. That escape hatch is nonisolated(unsafe):

class GamePiece {
  nonisolated(unsafe) static var energy = 100
}

While 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 nicely, that’s nice. But it surely’s additionally dangerous; you’re now taking over 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 eliminate it as quickly as attainable in favor of isolating your international mutable state to an actor.

Observe that in Swift 6.1 you could possibly make GamePiece an actor and the Swift compiler will let you have static 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 predominant department so I’d count on that Swift 6.2 emits an applicable error for having a static var on an actor.

Resolving the error for shared situations

While you’re working with a shared occasion, you usually don’t want the static var to be a var in any respect. When that’s the case, you possibly can truly resolve the unique error fairly simply:

class AuthProvider {
  static let shared = AuthProvider()

  personal init() {}
}

Make the property a let as a substitute of a var and Static property 'shared' isn't concurrency-safe as a result of it's nonisolated international shared mutable state goes away.

A brand new error will seem although…

Static property ‘shared’ 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’ isn’t concurrency-safe as a result of non-‘Sendable’ sort might have shared mutable state

Whereas the brand new error sounds lots 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’ve 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 activity would work together with the very same occasion. So if AuthProvider can’t deal with that appropriately, we’re in hassle.

The way in which to repair this, is to make AuthProvider a Sendable sort. When you’re undecided that you simply totally perceive Sendable simply but, be sure 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 have the ability to do the next:

closing class AuthProvider: Sendable {
  static let shared = AuthProvider()

  personal 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?

closing class AuthProvider: Sendable {
  static let shared = AuthProvider()

  // Saved property 'currentToken' of 
  // 'Sendable'-conforming class 'AuthProvider' is mutable
  personal var currentToken: String?

  personal init() {}
}

The compiler doesn’t enable our Sendable sort to have mutable state. It doesn’t matter that this state is personal, it’s merely not allowed.

Utilizing nonisolated(unsafe) as an escape hatch once more

If we’ve a shared occasion with mutable state, we’ve a number of choices obtainable 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()

  personal var currentToken: String?

  personal init() {}
}

This works however it’s in all probability the worst choice we’ve as a result of it doesn’t shield our mutable state from knowledge races.

Leveraging a worldwide actor to make AuthProvider Sendable

Alternatively, we may apply isolate our sort to the primary actor identical to we did with our static var:

// we will isolate our class
@MainActor
class AuthProvider {
  static let shared = AuthProvider()

  personal var currentToken: String?

  personal init() {}
}

// or simply the shared occasion
class AuthProvider {
  @MainActor
  static let shared = AuthProvider()

  personal var currentToken: String?

  personal init() {}
}

The professionals and cons of this options are the identical as they had been for the static var. If we principally use AuthProvider from the primary actor that is effective, but when we incessantly must work with AuthProvider from different isolation contexts it turns into a little bit of a ache.

Making AuthProvider an actor

My most popular resolution 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()

  personal var currentToken: String?

  personal init() {}
}

Actors in Swift are all the time Sendable which signifies that an actor can all the time be used as a static let.

There’s another escape hatch…

Let’s say we will’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.

Possibly you’ve had AuthProvider in your undertaking 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 may be achieved as follows:

closing class AuthProvider: @unchecked Sendable {
  static let shared = AuthProvider()

  personal var currentToken: String?

  personal init() {}
}

An escape hatch like this needs to be used fastidiously and may ideally be thought of a brief repair. The compiler received’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 guarantee that your singletons and shared situations are protected to make use of from a number of duties (isolation contexts) on the identical time.

On this submit, you’ve seen a number of methods to eliminate two shared occasion associated errors.

First, you noticed how one can have static var members in a approach that’s concurrency-safe by leveraging actor isolation.

Subsequent, you noticed that static let is one other approach to have a concurrency-safe static member so long as the kind of your static let is concurrency-safe. That is what you’ll usually use to your shared situations.

I hope this submit has helped you grasp static members and Swift 6 a bit higher, and that you simply’re now in a position to leverage actor isolation the place wanted to appropriately have international state in your apps.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles