guptron guptron - 5 months ago 24
Swift Question

In Swift, why does assigning to a static variable also invoke its getter

I understand that in Swift, static vars are implicitly lazy:

But I'm not clear on why this happens:

protocol HatType {}

class Hat: HatType {
init() { print("real hat") }

class MockHat: HatType {
init() { print("mock hat") }

struct HatInjector {
static var hat: HatType = Hat()

HatInjector.hat = MockHat()

// Output:
// real hat
// mock hat

What I'm seeing is that the assignment to the static var, is also invoking the getter in a sense. This isn't intuitive to me. What is happening here? Why doesn't the assignment only happen?

Answer Source

This is because static and global stored variables are currently (this is all subject to change) only given one accessor by the compiler – unsafeMutableAddressor, which gets a pointer to the variable's storage (this can be seen by examining the SIL or IR emitted).

This accessor simply:

  1. Gets a pointer to a compiler-generated global flag determining whether the static variable has been initialised.

  2. Calls swift_once with this pointer, along with a function that initialises the static variable (this is the initialiser expression you give it, i.e = Hat()). On Apple platforms, swift_once simply forwards onto dispatch_once_f.

  3. Returns a pointer to the static variable's storage, which the caller is then free to read and mutate – as the storage has static lifetime.

So it does (more or less) the equivalent of the Objective-C thread-safe lazy initialisation pattern:

+(Hat*) hat {

    static Hat* sharedHat = nil;
    static dispatch_once_t oncePredicate;

    dispatch_once(&oncePredicate, ^{
        sharedHat = [[Hat alloc] init];

    return sharedHat;

The main difference being that Swift gives back a pointer to the storage of the sharedHat (a pointer to a reference), rather than the sharedHat itself (just a reference to the instance).

Because this is the one and only accessor for static and global stored variables, in order to perform an assignment, Swift needs to call it in order to get the pointer to the storage. Therefore, if it wasn't loaded already – it will be loaded from its initialiser expression, before then being set to another value.

This behaviour is somewhat unintuitive, and has been filed as a bug. As Jordan Rose says in the comments of the report:

This is currently by design, but it might be worth changing the design.

So this behaviour could well change in a future version of the language.