在 Swift 中重新初始化一个延迟初始化变量

我有一个初始化为:

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

问题是,在某个时候,我需要重置这个 aClient变量,这样当 ClinetSession.shared()改变时它就可以再次初始化。但是如果我将类设置为可选的 Clinet?,LLVM 会在我试图将它设置为 nil时给我一个错误。如果我只是重置它在代码中的某个地方使用 aClient = Clinet(ClinetSession.shared()),它将以 EXEC_BAD_ACCESS结束。

有没有一种方法可以使用 lazy和被允许重置自己?

35709 次浏览

惰性是明确的一次性初始化。您想要采用的模型可能只是一个初始化按需模型:

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


var _aClient:Client?

现在只要 _aClientnil,它就会被初始化并返回。它可以通过设置 _aClient = nil来重新初始化

这允许将属性设置为 nil以强制重新初始化:

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
}

编辑: 根据 Ben Leggiero 的回答,在 Swift 3中,惰性变量可以是 nilable。 编辑2: 看起来 nilable 懒人变种人已经不复存在了。

很晚才来参加派对,甚至不确定这是否与 Swift 3有关,但我们开始吧。David 的回答很好,但是如果您想创建许多惰性的可以为空的变量,那么您将不得不编写一个相当大的代码块。我正在尝试创建一个封装这种行为的 ADT。以下是我目前为止得到的信息:

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

然后,您可以像下面这样声明和使用属性:

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

有些事情我还不喜欢,但不知道如何改进:

  • 您必须向初始化程序传递一个构造函数,它看起来很丑陋。不过,它的优点是,您可以确切地指定如何创建新对象。
  • 每次想要使用属性时都调用 get()是非常糟糕的。如果这是一个计算属性,而不是一个函数,但是计算属性不能发生变化,那么会稍微好一点。
  • 为了消除调用 get()的需要,您必须通过 ClearableLazy的初始化器扩展您想要使用它的每个类型。

如果有人想从这里接手,那就太棒了。

因为 lazy的行为在 Swift 4中发生了变化,我编写了一些 struct,它们给出了非常具体的行为,这些行为在不同的语言版本之间不应该改变。我把这些放在 GitHub 上,使用 BH-1-PD许可证: https://github.com/RougeWare/Swift-Lazy-Patterns

ResettableLazy

下面是与这个问题相关的一个,它为您提供了一种惰性初始化值、缓存该值并销毁它的方法,以便以后可以对其进行惰性重新初始化。

注意,这需要 Swift 5.1! 对于 Swift 4版本,请参阅 该回购协议的1.1.1版本

这种方法的用法很简单:

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

这将印刷:

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

如果有复杂的初始化器逻辑,可以将其传递给属性包装器:

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

您还可以直接使用它(而不是作为属性包装器) :

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

这两本书都将印刷:

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

这个问题的答案已经更新; 它的原始解决方案不再适用于 Swift 4和更新的版本。

相反,我建议您使用上面列出的解决方案之一,即 @ PBosman 的解决方案

以前,这个答案取决于一个错误的行为。这个答案的旧版本,它的行为,以及为什么它是一个错误都在 Swift bug SR-5172的文本和评论中描述了(这个问题已经在2017-07-14与 公关费10911英镑一起解决了) ,很明显,这种行为从来都不是故意的。

这个解决方案在 Swift bug 的文本中,也在 这个答案的历史中,但是因为它是一个 bug 漏洞,在 Swift 3.2 + 我建议你这样做。中不起作用

这里有一些很好的答案。 < br > 在很多情况下,重置一个惰性变量的确是可取的。

我认为,您还可以定义一个闭包来创建 client 并使用这个闭包重置惰性变量。就像这样:

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

如果目标是重新初始化一个惰性属性,但不一定将其设置为 nil,那么每次读取该值时都可以避免条件检查:

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

测试:

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"

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

使用方法如下:

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

这产生了以下结果:

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和属性包装器

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

我们通过将缓存设置为任意值来重置缓存:

var game = Game()
print(game.scores)
print(game.scores)
game.scores = []
print(game.scores)
print(game.scores)

我把 @David Berry's答案做成了财产包装纸。对于需要应用大小更改但又想保持其配置状态的 UI 组件,可以很好地加载这些组件。

@propertyWrapper class Reloadable<T: AnyObject> {
  

private let initializer:  (() -> T)
private var _wrappedValue: T?
var wrappedValue: T {
if _wrappedValue == nil {
_wrappedValue = initializer()
}
return _wrappedValue!
}
  

init(initializer: @escaping (() -> T)) {
self.initializer = initializer
}
  

func nuke() {
_wrappedValue = nil
}
}

下面是一个 CAShapeLayer的例子。像这样设置变量:


@Reloadable<CAShapeLayer>(initializer: {
Factory.ShapeLayer.make(fromType: .circle(radius: Definitions.radius, borderWidth: Definitions.borderWidth)) // this factory call is just what I use personally to build my components
}) private var circleLayer


当你想重新载入你的视图时,只需要调用:

_circleLayer.nuke()

然后,您可以使用 var circleLayer,正如您通常会在您的重新布局例程,在此之上,它将得到重新初始化。

PS: 我为我在自己的项目中使用的文件 https://gist.github.com/erikmartens/b34a130d11b62400ab13a59a6c3dbd91做了一个要点

这似乎是一个相当糟糕的代码气味。一些奇怪的事情正在发生:

var _aClient = Clinet(ClinetSession.shared())

什么是 ClinetSession.shared()

这看起来像是一个静态函数,在每次调用时返回一个新的 ClinetSession实例。

所以,难怪你没有看到这个对象的变化。在我看来,它就像一个破碎的单例模式。

class ClinetSession {
static func shared() -> Self {
ClinetSession()
}
}

试着这样做:

class ClinetSession {
static let shared = ClinetSession()
    

var value: Int = 0
}

现在如果你改变 ClinetSession value你会看到它。

就我所知,没有必要在这里重置一个惰性属性。

这里有一个完整的例子。顺便说一下,如果你没有 ClinetSession,那么写一个包装器来控制它。

class Clinet {
let clinetSession: ClinetSession
init(_ clinetSession: ClinetSession) {
self.clinetSession = clinetSession
}
    

var delegate: P?
}


class ClinetSession {
static let shared = ClinetSession()
var value = 0
}


protocol P { }


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

mutating func updateSession() {
aClient.clinetSession.value = 10
}
    

}


var test = Test()
test.updateSession()
print(test.aClient.clinetSession.value)


// prints 10

注意: 如果您不想使用单例,那么不要使用 shared()作为构造函数,因为这是一个约定。但是接下来要由您来确保传入的引用与要变异的引用相同。那是你的工作。单例只是确保只有一个实例,所以这会变得更简单,但也有其优缺点。