Trigger lazy initializer again in Swift by setting

2020-05-26 02:41发布

问题:

I want a lazily-initialized property whose initializer I can invoke again if I set the property to nil.

If I define my property this way:

lazy var object = { /*init code*/ }()

...and later invoke the property, the initializer is triggered once. However, if I set object to nil later in my program, the initializer is not invoked again. How can I do that in Swift?

I looked into computed properties but they don't actually store values, so whenever I invoke the variable, the computation or initialization always occurs. I want to compute only whenever the property is nil.

回答1:

The lazy property initializer is responsible of initializing the property the first time it is accessed in read mode. Setting to nil has no effect on the initialization status - it's just a valid value the property stores.

You can mimic a lazy initialization with 3 properties:

  • a private initializer, implemented as a computed property (or a closure if you prefer)
  • a private backing property, storing the actual value
  • a non private property, which is the one you actually use in your code

The code looks like this:

class MyClass {
    private var _myPropInitializer: Int {
        return 5
    }

    private var _myProp: Int?

    var myProp: Int? {
        get {
            if self._myProp == nil {
                self._myProp = self._myPropInitializer
            }
            return _myProp!
        }
        set {
            _myProp = newValue
        }
    }
}
  • the initializer property returns a computed value for the variable when it needs to be initialized, which is the 5 integer in the above example
  • myProp is an optional integer (to be able to store a nil):
    • on set, it will store the new value in the _myProp property
    • on get, if _myProp is nil, it invokes the initializer, assigning it to _myProp, and it returns its value

If you want to reuse that pattern, it's better to put everything in a class:

class Lazy<T> {
    private let _initializer: () -> T
    private var _value: T?
    var value: T? {
        get {
            if self._value == nil {
                self._value = self._initializer()
            }
            return self._value
        }
        set {
            self._value = newValue
        }
    }

    required init(initializer: () -> T) {
        self._initializer = initializer
    }
}

Note: a struct is not usable because setting a property inside a property getter is not allowed, whereas in a class it is.

Then you can use it as follows:

class MyTestClass {
    var lazyProp: Lazy<Int>

    init() {
        self.lazyProp = Lazy( { return 5 } )
    }
}

Some tests in playground:

var x = MyTestClass()
x.lazyProp.value // Prints {Some 5}
x.lazyProp.value = nil
x.lazyProp._value // Prints nil
x.lazyProp.value // Prints {Some 5}

The downside is that you have to access to the actual property as x.lazyProp.value and not as x.lazyProp.



回答2:

Here's a lazy pattern I use when your object can only ever be nil or a computed value. It requires only 2 properties:

var todayPredicate: NSPredicate! {
    set {
        guard newValue == nil else {return} // could throw here if required
        self._todayPredicateLazyBacker = nil
    }
    get {
        guard self._todayPredicateLazyBacker == nil else {return self. _todayPredicateLazyBacker}

        // construct your today predicate
        let predicate = ...

        self._todayPredicateLazyBacker = predicate
        return self._todayPredicateLazyBacker
    }
}
private var _todayPredicateLazyBacker: NSPredicate?

todayPredicate is constructed only once when it is read for the first time (lazy).

So why would you ever want to set todayPredicate to nil? In this example you are probably observing for the day changing because todayPredicate must always represent today. In your observer code you would simply do this, for example...

self.todayPredicate = nil
self.loadEvents()