Setting lazy static variable first initializes then assigns?
Asked Answered
H

1

5

I realize that static variables are implicitly lazy, which is really great. Doing the below will not create the instance until it's first called:

static var test = Test()

However, assigning a new instance to the static variable initializes the original, then assigns the new instance which is troubling for me:

SomeType.test = AnotherTest() //Initializes Test then AnotherTest type

To give more context on what I'm trying to do, I'm trying to setup a pure Swift dependency injection using this article. It's not working so well when swapping the types out in my unit tests because the original type always gets initialized when assigning the mock type.

Here's a more fuller, playground sample:

protocol MyProtocol { }

class MyClass: MyProtocol {
    init() { print("MyClass.init") }
}

////

struct MyMap {
    static var prop1: MyProtocol = MyClass()
}

protocol MyInject {

}

extension MyInject {
    var prop1: MyProtocol { return MyMap.prop1 }
}

////

class MyMock: MyProtocol {
    init() { print("MyMock.init") }
}

// Swapping types underneath first initializes
// original type, then mock type :(
MyMap.prop1 = MyMock()

prints: MyClass.init
prints: MyMock.init

How can I make MyMap.prop1 = MyMock() not first initialize the original MyClass first?

Hime answered 12/4, 2017 at 15:35 Comment(0)
R
3

You need lazy loading. Try this:

struct MyMap {
    private static var _prop1: MyProtocol?
    static var prop1: MyProtocol {
        get { return _prop1 ?? MyClass() }
        set(value) { _prop1 = value }
    }
}

Or this:

struct MyMap {
    private static var _prop1: MyProtocol?
    static var prop1: MyProtocol {
        get {
            if _prop1 == nil {
                _prop1 = MyClass()
            }
            return _prop1!
        }
        set(value) { _prop1 = value }
    }
}
Raft answered 12/4, 2017 at 18:35 Comment(7)
Whoa, why does this work??!.. Is this a bug in Swift or was this intentional?Hime
Its as intended by swift. A var needs a value as long as it exists. This value may be nil or a concrete one. Your var can not be nil by your definition, therefore you have to specify a value (or compile throw an error). It doesn't matter if you call the get or set. Swift has to ensure that your var has a value before working with it. Thats what apple call "swift is save".Raft
@codealchimist But static stored properties are lazily loaded – it shouldn't have to call the property initialiser if it's first assigned to before being loaded. But that's just currently how Swift behaves.Langsdon
It's also worth noting that static stored property initialisers are thread safe, whereas lazily loading like in your second example is not thread safe.Langsdon
@Langsdon True both comments. There was a bug report for it too, maybe it will change in future swift versions and static isn't lazy anymore or it works as expected from a lazy var. Think it will be the first one for compatibility reasons. We will see..Raft
Thx @Langsdon for pointing out the thread-safety. I wrapped the getter in a serial queue and worked great. I was able to prove that it worked by calling MyMap.prop1 with DispatchQueue.concurrentPerform(iterations: 1000).Hime
For reference: ``` get { guard _prop1 == nil else { return _prop1! }; queue.sync { if _prop1 == nil { _prop1 = MyClass() } }; return _prop1! } ```Hime

© 2022 - 2024 — McMap. All rights reserved.