Initialisieren Sie eine verzögert initialisierte Variable in Swift neu

73

Ich habe eine Variable, die wie folgt initialisiert wurde:

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

Das Problem ist, dass ich diese aClientVariable irgendwann zurücksetzen muss , damit sie beim Ändern erneut initialisiert werden kann ClinetSession.shared(). Wenn ich die Klasse jedoch auf optional setze Clinet?, gibt LLVM eine Fehlermeldung aus, wenn ich versuche, sie auf zu setzen nil. Wenn ich es nur irgendwo im Code mit zurücksetze aClient = Clinet(ClinetSession.shared()), endet es mit EXEC_BAD_ACCESS.

Gibt es eine Möglichkeit, sich lazyselbst zurückzusetzen?

Cai
quelle
Nur weil ich beim Lesen des Codes gestolpert bin: Ist es Clinetund ClinetSession absichtlich oder ist es ein Tippfehler?
luk2302
1
@ luk2302 Tippfehler, aber ich denke, es hat die Leute doch nicht davon abgehalten, meine Frage zu lösen. :)
Cai

Antworten:

95

faul ist explizit nur für die einmalige Initialisierung. Das Modell, das Sie übernehmen möchten, ist wahrscheinlich nur ein Initialisierungsmodell bei Bedarf:

var aClient:Client {
    if(_aClient == nil) {
        _aClient = Client(ClientSession.shared())
    }
    return _aClient!
}

var _aClient:Client?

Wann immer dies der Fall _aClientist nil, wird es initialisiert und zurückgegeben. Es kann durch Einstellen neu initialisiert werden_aClient = nil

David Berry
quelle
Das ist klug! Übrigens denke ich, wir müssen möglicherweise self._aClientin der verwenden get{}. Manchmal kann der Swift-Compiler in einigen Fällen eine Variable finden, wenn die self.fehlt.
Cai
6
Ich hatte mir in Swift 2 etwas Besseres erhofft. Was Sie wirklich wollen, ist eine Variable mit einem Setter, der nur das Setzen auf Null erlaubt, und Getter-Code, der nur aufgerufen wird, wenn die Eigenschaft Null ist. Wie var aClient: Client {setnil; getnil {aClient = Client (ClientSession.shared ())}} getnil wird aufgerufen, wenn Sie die Variable erhalten, nachdem Sie sie auf nil gesetzt haben.
gnasher729
1
@ gnasher729 Hurra für Swift 3! Siehe meine Antwort für genau das :)
Ben Leggiero
Schöne Antwort ... Diese Methode hat gut funktioniert, um den GMSCircle einzustellen, den ich ursprünglich als Lazy var erstellt habe. Danke!
BharathRao
Wenn Sie var _aClient verwenden: Client! dann können Sie _aClient einfacher im Getter konfigurieren.
Malhal
45

Da sich das Verhalten von lazyin Swift 4 geändert hat , habe ich einige structs geschrieben, die ein sehr spezifisches Verhalten ergeben, das sich zwischen den Sprachversionen niemals ändern sollte. Ich habe diese auf GitHub unter der BH-1-PD- Lizenz bereitgestellt : https://github.com/RougeWare/Swift-Lazy-Patterns

ResettableLazy

Hier ist die für diese Frage relevante, mit der Sie einen Wert träge initialisieren, diesen Wert zwischenspeichern und zerstören können, damit er später träge neu initialisiert werden kann.

Beachten Sie, dass dies Swift 5.1 erfordert! Für die Swift 4-Version siehe Version 1.1.1 dieses Repos .

Die einfache Verwendung ist sehr einfach:

@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!"

Dies wird gedruckt:

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

Wenn Sie über eine komplexe Initialisierungslogik verfügen, können Sie diese an den Eigenschaften-Wrapper übergeben:

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!"

Sie können es auch direkt verwenden (als Property Wrapper installiert):

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!"

Diese werden beide gedruckt:

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

Diese Antwort wurde aktualisiert. Die ursprüngliche Lösung funktioniert nicht mehr in Swift 4 und höher.

Stattdessen empfehle ich Ihnen, eine der oben aufgeführten Lösungen oder die Lösung von @ PBosman zu verwenden

Zuvor hing diese Antwort vom Verhalten ab, das ein Fehler war. Sowohl diese alte Version dieser Antwort als auch ihr Verhalten und warum es sich um einen Fehler handelt, werden im Text und in den Kommentaren des Swift-Fehlers SR-5172 (der ab dem 14.07.2017 mit PR # 10.911 behoben wurde ) beschrieben, und es ist klar dass dieses Verhalten nie beabsichtigt war.

Diese Lösung befindet sich im Text dieses Swift-Fehlers und auch im Verlauf dieser Antwort . Da es sich jedoch um einen Fehler-Exploit handelt, der in Swift 3.2+ nicht funktioniert, empfehle ich, dies nicht zu tun.

Ben Leggiero
quelle
2
IMHO, dies ist ein sauberer Weg, um dies zu erreichen als die akzeptierte Antwort.
Timgcarlson
1
+1 für die Verwendung von !, wenn man dieses Muster verwenden würde, müsste jeder Aufrufer dieser Variablen das noch auspacken Optional. ZBprint(aClient) // prints "Optional(Clinet)\n"
pxpgraphics
1
Ich habe es heute Morgen auf einem Spielplatz ausprobiert und hier hochgeladen: gist.github.com/pxpgraphics/4cfb7e02b6be7a583bf5f8a3ccbcd29a Vielleicht hängt dies mit der String-Interpolation zusammen:]
pxpgraphics
1
Ich glaube, ich habe diesen Bug-Exploit versehentlich verwendet ... Ich fand es sehr seltsam, dass langjähriger Code erst vor kurzem Abstürze verursachte (kürzlich auf Swift 4 aktualisiert). Gibt es starke Vorteile für die Lösung, mit der Sie in Ihrer Bearbeitung verknüpft haben, gegenüber der akzeptierten Lösung? Die akzeptierte Lösung scheint meinen Anforderungen zu entsprechen. Die von Ihnen verknüpfte Lösung scheint zu komplex zu sein, und ich sehe keinen Vorteil darin.
Jake T.
1
@ Ben Leggiero Ich schätze die Bestätigung :) obwohl ich, wie ich in meiner Antwort erwähnt habe, mit meiner Version nicht ganz zufrieden bin. Es würde mich interessieren, was Sie haben, wenn Sie bereit wären, es zu teilen?
Phlippie Bosman
7

EDIT: Gemäß der Antwort von Ben Leggiero können faule Vars nilin Swift 3 fähig sein. EDIT 2: Scheint, als ob nilfaule Vars nicht mehr in der Lage sind.

Sehr spät zur Party und nicht einmal sicher, ob dies in Swift 3 relevant sein wird, aber hier geht es weiter. Davids Antwort ist gut, aber wenn Sie viele faule Null-Vars erstellen möchten, müssen Sie einen ziemlich umfangreichen Codeblock schreiben. Ich versuche, ein ADT zu erstellen, das dieses Verhalten kapselt. Folgendes habe ich bisher:

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 }
}

Sie würden dann Eigenschaften wie diese deklarieren und verwenden:

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

Es gibt Dinge, die mir noch nicht gefallen, die ich aber nicht verbessern kann:

  • Sie müssen einen Konstruktor an den Initialisierer übergeben, der hässlich aussieht. Es hat jedoch den Vorteil, dass Sie genau festlegen können, wie neue Objekte erstellt werden sollen.
  • Der Aufruf get()auf einer Eigenschaft jedes Mal , die Sie verwenden möchten es ist schrecklich. Es wäre etwas besser, wenn dies eine berechnete Eigenschaft wäre, keine Funktion, aber berechnete Eigenschaften können nicht mutieren.
  • Um das Aufrufen zu vermeiden get(), müssen Sie jeden Typ, für den Sie dies verwenden möchten, um Initialisierer für erweitern ClearableLazy.

Wenn jemand Lust hat, es von hier abzuholen, wäre das großartig.

Phlippie Bosman
quelle
1
Dies ist im Wesentlichen, was ich jetzt in Swift 4
mache
4

Auf diese Weise kann die Eigenschaft so eingestellt werden, dass nileine Neuinitialisierung erzwungen wird:

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
}
William Entriken
quelle
3

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
    }
}

So verwenden Sie:

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

Dies erzeugt die folgende Ausgabe:

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 und 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]
}

Wir setzen den Cache zurück, indem wir ihn auf einen beliebigen Wert setzen:

var game = Game()
print(game.scores)
print(game.scores)
game.scores = []
print(game.scores)
print(game.scores)
Karam
quelle
Schauen Sie sich die Alternative zum Property Wrapper an. Die Syntax ist jetzt viel sauberer.
Karam
2

Hier gibt es einige gute Antworten.
Das Zurücksetzen eines Lazy Var ist in der Tat in vielen Fällen wünschenswert.

Ich denke, Sie können auch einen Abschluss definieren, um einen Client zu erstellen und Lazy Var mit diesem Abschluss zurückzusetzen. Etwas wie das:

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
Puneet Sharma
quelle
2

Wenn das Ziel darin besteht, eine faule Eigenschaft neu zu initialisieren, aber nicht unbedingt auf Null zu setzen (Gebäude aus Phlippie Bosman und Ben Leggiero), vermeiden Sie hiermit bedingte Überprüfungen jedes Mal, wenn der Wert gelesen wird:

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()
    }
}

Zu testen:

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"
Jose Santos
quelle