6

I understand that in Swift, static vars are implicitly lazy: https://stackoverflow.com/a/34667272/1672161

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?

Community
  • 1
  • 1
abc123
  • 8,043
  • 7
  • 49
  • 80
  • Is this a class project? Someone else just posted http://stackoverflow.com/questions/43373932/setting-lazy-static-variable-first-initializes-then-assigns a few minutes ago asking the same thing. – rmaddy Apr 12 '17 at 15:53
  • ha! must be a coincidence! @rmaddy. Closing as that one has more discussion, so better to continue with that one – abc123 Apr 12 '17 at 20:16
  • I personally don't think this is a duplicate – the linked Q&A doesn't ask about the *why* of the behaviour (although I agree it's not great having the explanation and solution split across different questions). But I don't think dupe marking is the best solution – I propose we re-open. – Hamish Apr 12 '17 at 20:29

2 Answers2

10

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:

  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 sharedHat (a pointer to a reference), rather than 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 initialised already – the accessor needs to first initialise it to its default value (as it has no idea what the caller is going to do with it), before the caller then sets it to another value.

This behaviour is indeed 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.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 1
    This answer is more than I could have hoped for! Thanks for the effort of putting this together. – abc123 Apr 13 '17 at 12:13
  • Happy to help @guptron :) – Hamish Apr 13 '17 at 12:58
  • Could you confirm whether my understanding is right or not? A global stored variable which have an accessor except for `private` exists in a static area, not heap area although I thought instance properties are stored in heap. Is it right? – Kazuya Tomita Jun 23 '17 at 04:36
  • And how about the memory management? I mean in heap area Swift uses ARC in order to do memory management. If global stored properties is in the static storage, how is the memory management done? If a certain class has some stored properties, when the instance of the class is instantiated Swift creates some values of the instance in the static area. And after removing the instance, what happens in the static area of theses variables? I think ARC takes care of only the heap, but is it wrong? – Kazuya Tomita Jun 24 '17 at 03:18
  • @KazuyaTomita Class instances are still allocated on the heap, and ARC continues the manage them as normal. If you, for example, have a global variable of class type, then what you have is some static storage for a *reference* to the given instance of that class. The `unsafeMutableAddressor` then returns a pointer to that static memory that holds the reference (so a pointer to a reference). Static memory doesn't need to be memory managed, because its lifetime is static, i.e tied to the lifetime of the process itself. – Hamish Jun 24 '17 at 11:35
  • Thank you for the reply. So, in the case the instance on the heap is not allocated, right? Because the instance has the pointer which refers the instance, ARC would regard it is referred. Is it right behavior? To me, it is strange... – Kazuya Tomita Jun 24 '17 at 11:48
  • @KazuyaTomita I'm not sure what you mean by "the instance on the heap is not allocated" – of course it is. We have a pointer to the static memory, which in turn holds a reference to the instance on the heap. When the reference is assigned to the global variable, the retain count of the instance is incremented. When the global variable is assigned some other reference (or `nil`), the retain count of the instance is decremented. – Hamish Jun 24 '17 at 12:40
  • Imagine we have the `class` named of `A`. And `A`have a variable `b`, namely `var b = B()` where `B` is another class. So, after we instantiate `A`, when we access `b`, the reference to `B`'s instance is created on the static storage. And now `b` has the pointer to the static storage which has the reference of the `B`'s instance. After `A` is deallocated, `b` which refers to the static storage is also removed. However, the reference on the static storage still refers to the instance of `B`. So, to me `B` doesn't seem to be deallocated. What of my explanation is wrong? – Kazuya Tomita Jun 24 '17 at 13:40
  • @KazuyaTomita From what you've described, you don't have any global or static variables in your example, so no static storage, – `b` is just an instance member of `A`. When the instance of `A` is deallocated, the retain count of `b` is decremented. Assuming you meant `static var b = B()`, then the lifetime of that `B` instance is independant of any `A` instances – it has static lifetime. – Hamish Jun 24 '17 at 13:50
0

Same solution as in Setting lazy static variable first initializes then assigns?

Try lazy loading:

struct HatInjector {
    private static var _hat: HatType?
    static var hat: HatType {
        get { return _hat ?? Hat() }
        set(value) { _hat = value }
    }
}

Or:

struct HatInjector {
    private static var _hat: HatType?
    static var hat: HatType {
        get {
            if _hat == nil {
                _hat = Hat()
            }
            return _hat!
        }
        set(value) { _hat = value }
    }
}

Reason: The static var in your code is not optional. Therefore when using it swift has to ensure that it isn't nil (swift is save!). Therefore the compiler request that you set an initial value. You can't define:

static var prop1: MyProtocol

This will result in a compiler error. If you define

static var prop1: MyProtocol?

it will be valid because it's a shortcut for

static var prop1: MyProtocol? = nil
Community
  • 1
  • 1
ObjectAlchemist
  • 1,109
  • 1
  • 9
  • 18
  • 2
    In the future, instead of posting a duplicate answer from a duplicate question, vote to close as a duplicate. – rmaddy Apr 12 '17 at 20:28