Re-initialize a lazy initialized variable in Swift

前端 未结 7 815
没有蜡笔的小新
没有蜡笔的小新 2020-12-04 11:08

I have a variable that initialized as:

lazy var aClient:Clinet = {
    var _aClient = Clinet(ClinetSession.shared())
    _aClient.delegate = self
    return          


        
相关标签:
7条回答
  • 2020-12-04 11:35

    Because the behavior of lazy changed in Swift 4, I wrote a few structs that give very specific behavior, which should never change between language versions. I put these on GitHub, under the BH-1-PD license: https://github.com/RougeWare/Swift-Lazy-Patterns

    ResettableLazy

    Here is the one relevant to this question, which gives you a way to lazily-initialize a value, cache that value, and destroy it so it can be lazily-reinitialized later.

    Note that this requires Swift 5.1! For the Swift 4 version, see version 1.1.1 of that repo.

    The simple usage of this is very straightforward:

    @ResettableLazy
    var myLazyString = "Hello, lazy!"
    
    print(myLazyString) // Initializes, caches, and returns the value "Hello, lazy!"
    print(myLazyString) // Just returns the value "Hello, lazy!"
    
    _myLazyString.clear()
    print(myLazyString) // Initializes, caches, and returns the value "Hello, lazy!"
    print(myLazyString) // Just returns the value "Hello, lazy!"
    
    myLazyString = "Overwritten"
    print(myLazyString) // Just returns the value "Overwritten"
    _myLazyString.clear()
    print(myLazyString.wrappedValue) // Initializes, caches, and returns the value  "Hello, lazy!"
    

    This will print:

    Hello, lazy!
    Hello, lazy!
    Hello, lazy!
    Hello, lazy!
    Overwritten
    Hello, lazy!
    

    If you have complex initializer logic, you can pass that to the property wrapper:

    func makeLazyString() -> String {
        print("Initializer side-effect")
        return "Hello, lazy!"
    }
    
    @ResettableLazy(initializer: makeLazyString)
    var myLazyString: String
    
    print(myLazyString) // Initializes, caches, and returns the value "Hello, lazy!"
    print(myLazyString) // Just returns the value "Hello, lazy!"
    
    _myLazyString.clear()
    print(myLazyString) // Initializes, caches, and returns the value "Hello, lazy!"
    print(myLazyString) // Just returns the value "Hello, lazy!"
    
    myLazyString = "Overwritten"
    print(myLazyString) // Just returns the value "Overwritten"
    _myLazyString.clear()
    print(myLazyString.wrappedValue) // Initializes, caches, and returns the value  "Hello, lazy!"
    

    You can also use it directly (instaed of as a property wrapper):

    var myLazyString = ResettableLazy<String>() {
        print("Initializer side-effect")
        return "Hello, lazy!"
    }
    
    print(myLazyString.wrappedValue) // Initializes, caches, and returns the value "Hello, lazy!"
    print(myLazyString.wrappedValue) // Just returns the value "Hello, lazy!"
    
    myLazyString.clear()
    print(myLazyString.wrappedValue) // Initializes, caches, and returns the value "Hello, lazy!"
    print(myLazyString.wrappedValue) // Just returns the value "Hello, lazy!"
    
    myLazyString.wrappedValue = "Overwritten"
    print(myLazyString.wrappedValue) // Just returns the value "Overwritten"
    _myLazyString.clear()
    print(myLazyString.wrappedValue) // Initializes, caches, and returns the value  "Hello, lazy!"
    

    These will both print:

    Initializer side-effect
    Hello, lazy!
    Hello, lazy!
    Initializer side-effect
    Hello, lazy!
    Hello, lazy!
    Overwritten
    Initializer side-effect
    Hello, lazy!
    

    This answer has been updated; its original solution no longer works in Swift 4 and newer.

    Instead, I recommend you use one of the solutions listed above, or @PBosman's solution

    Previously, this answer hinged on behavior which was a bug. Both that old version of this answer, its behavior, and why it's a bug are described in the text and comments of Swift bug SR-5172 (which has been resolved as of 2017-07-14 with PR #10,911), and it's clear that this behavior was never intentional.

    That solution is in that Swift bug's text, and also in the history of this answer, but because it's a bug exploit that doesn't work in Swift 3.2+ I recommend you do not do that.

    0 讨论(0)
  • 2020-12-04 11:37

    EDIT: As per Ben Leggiero's answer, lazy vars can be nilable in Swift 3. EDIT 2: Seems like nilable lazy vars are no more.

    Very late to the party, and not even sure if this will be relevant in Swift 3, but here goes. David's answer is good, but if you want to create many lazy nil-able vars, you will have to write a pretty hefty block of code. I'm trying to create an ADT that encapsulates this behaviour. Here's what I've got so far:

    struct ClearableLazy<T> {
        private var t: T!
        private var constructor: () -> T
        init(_ constructor: @escaping () -> T) {
            self.constructor = constructor
        }
        mutating func get() -> T {
            if t == nil {
                t = constructor()
            }
            return t
        }
        mutating func clear() { t = nil }
    }
    

    You would then declare and use properties like this:

    var aClient = ClearableLazy(Client.init)
    aClient.get().delegate = self
    aClient.clear()
    

    There are things I don't like about this yet, but don't know how to improve:

    • You have to pass a constructor to the initializer, which looks ugly. It has the advantage, though, that you can specify exactly how new objects are to be created.
    • Calling get() on a property every time you want to use it is terrible. It would be slightly better if this was a computed property, not a function, but computed properties cannot be mutating.
    • To eliminate the need to call get(), you have to extend every type you want to use this for with initializers for ClearableLazy.

    If someone feels like picking it up from here, that would be awesome.

    0 讨论(0)
  • 2020-12-04 11:37

    There are some good answers here.
    Resetting a lazy var is indeed, desirable in a lot of cases.

    I think, you can also define a closure to create client and reset lazy var with this closure. Something like this:

    class ClientSession {
        class func shared() -> ClientSession {
            return ClientSession()
        }
    }
    
    class Client {
        let session:ClientSession
        init(_ session:ClientSession) {
            self.session = session
        }
    }
    
    class Test {
        private let createClient = {()->(Client) in
            var _aClient = Client(ClientSession.shared())
            print("creating client")
            return _aClient
        }
    
        lazy var aClient:Client = createClient()
        func resetClient() {
            self.aClient = createClient()
        }
    }
    
    let test = Test()
    test.aClient // creating client
    test.aClient
    
    // reset client
    test.resetClient() // creating client
    test.aClient
    
    0 讨论(0)
  • 2020-12-04 11:38

    This allows setting the property to nil to force reinitialization:

    private var _recordedFileURL: NSURL!
    
    /// Location of the recorded file
    private var recordedFileURL: NSURL! {
        if _recordedFileURL == nil {
            let file = "recording\(arc4random()).caf"
            let url = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent(file)
            NSLog("FDSoundActivatedRecorder opened recording file: %@", url)
            _recordedFileURL = url
        }
        return _recordedFileURL
    }
    
    0 讨论(0)
  • 2020-12-04 11:44

    Swift 5.1:

    class Game {
        private var _scores: [Double]? = nil
    
        var scores: [Double] {
            if _scores == nil {
                print("Computing scores...")
                _scores = [Double](repeating: 0, count: 3)
            }
            return _scores!
        }
    
        func resetScores() {
            _scores = nil
        }
    }
    

    Here is how to use:

    var game = Game()
    print(game.scores)
    print(game.scores)
    game.resetScores()
    print(game.scores)
    print(game.scores)
    

    This produces the following output:

    Computing scores...
    [0.0, 0.0, 0.0]
    [0.0, 0.0, 0.0]
    Computing scores...
    [0.0, 0.0, 0.0]
    [0.0, 0.0, 0.0]
    

    Swift 5.1 and Property Wrapper

    @propertyWrapper
    class Cached<Value: Codable> : Codable {
        var cachedValue: Value?
        var setter: (() -> Value)?
    
        // Remove if you don't need your Value to be Codable    
        enum CodingKeys: String, CodingKey {
            case cachedValue
        }
    
        init(setter: @escaping () -> Value) {
            self.setter = setter
        }
    
        var wrappedValue: Value {
            get {
                if cachedValue == nil {
                    cachedValue = setter!()
                }
                return cachedValue!
            }
            set { cachedValue = nil }
        }
    
    }
    
    class Game {
        @Cached(setter: {
            print("Computing scores...")
            return [Double](repeating: 0, count: 3)
        })
        var scores: [Double]
    }
    

    We reset the cache by setting it to any value:

    var game = Game()
    print(game.scores)
    print(game.scores)
    game.scores = []
    print(game.scores)
    print(game.scores)
    
    0 讨论(0)
  • 2020-12-04 11:49

    If the objective is to re-initialize a lazy property but not necessarily set it to nil, Building from Phlippie Bosman and Ben Leggiero, here is something that avoids conditional checks every time the value is read:

    public struct RLazy<T> {
        public var value: T
        private var block: () -> T
        public init(_ block: @escaping () -> T) {
            self.block = block
            self.value = block()
        }
        public mutating func reset() {
            value = block()
        }
    }
    

    To test:

    var prefix = "a"
    var test = RLazy { () -> String in
        return "\(prefix)b"
    }
    
    test.value         // "ab"
    test.value = "c"   // Changing value
    test.value         // "c"
    prefix = "d"
    test.reset()       // Resetting value by executing block again
    test.value         // "db"
    
    0 讨论(0)
提交回复
热议问题