Utilizing singletons in Swift 6 – Donny Wals

Utilizing singletons in Swift 6 – Donny Wals


Singletons typically talking get a nasty 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, generally you’ll have singletons. Or, extra doubtless, 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 cases as problematic, and we’ll see what you are able to do to fulfill the Swift 6 compiler. We’ll run by means of a number of completely different errors that you may get on your shared cases relying on the way you’ve structured your code.

Static property ‘shared’ is just not 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' is just not concurrency-safe as a result of it 
  // is nonisolated international shared mutable state
  static var shared = AuthProvider()

  non-public init() {}
}

class GamePiece {
  // Static property 'energy' is just not 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. Which means 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 must always guarantee that we are able to safely mutate and meaning we have to isolate our mutable state someway.

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 primary actor

One strategy 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 sensible when GamePiece is a category that’s designed to carefully work with our UI layer. After we solely ever work with GamePiece from the UI, it is sensible 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 strategy typically results in extra concurrency in your app, and it’ll make your code extra complicated general.

There’s a second possibility that we are able to attain for, however it’s one which it’s best to solely use if constraining your sort to a worldwide actor is not sensible.

It’s nonisolated(unsafe).

Permitting static var with nonisolated(unsafe)

Typically you’ll know that your code is protected. For instance, you may 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 fundamental actor remoted. This is sensible 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 activity.

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

In an current app, you often need to be very cautious about introducing new actors. And if constraining to the primary actor isn’t an possibility 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
}

If you mark a static var as nonisolated(unsafe) the compiler will now 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 effectively, that’s nice. But it surely’s additionally dangerous; you’re now taking over the handbook accountability of stop 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 potential in favor of isolating your international mutable state to an actor.

Word that in Swift 6.1 you would make GamePiece an actor and the Swift compiler will 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 fundamental 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

If 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 possibly can truly 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' is just not 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’ is just not 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’ is just not concurrency-safe as a result of non-‘Sendable’ sort could have shared mutable state

Whereas the brand new error sounds quite a bit 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 got 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 bother.

The best way to repair this, is to make AuthProvider a Sendable sort. When you’re undecided that you simply absolutely perceive Sendable simply but, be certain that 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 have to be a ultimate 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 of do the next:

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

ultimate 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’ve got a shared occasion with mutable state, we’ve got a number of choices obtainable 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 however it’s in all probability the worst possibility we’ve got as a result of it doesn’t shield our mutable state from knowledge races.

Leveraging a worldwide actor to make AuthProvider Sendable

Alternatively, we might 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 have been for the static var. If we largely 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 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 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 current 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 performed as follows:

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

  non-public var currentToken: String?

  non-public init() {}
}

An escape hatch like this must be used rigorously and may ideally be thought-about a brief repair. The compiler received’t complain however you’re open to data-races that the compiler can assist stop 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 cases 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 do away with 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 strategy 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 simply’re now capable of leverage actor isolation the place wanted to appropriately have international state in your apps.

Leave a Reply

Your email address will not be published. Required fields are marked *