【问题标题】:Re-initialize a lazy initialized variable in Swift在 Swift 中重新初始化一个惰性初始化变量
【发布时间】:2014-08-01 03:50:26
【问题描述】:

我有一个变量初始化为:

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

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

有没有办法可以使用lazy 并允许自己重置?

【问题讨论】:

  • 只是因为我偶然阅读了代码:是故意的ClinetClinetSession 还是一个错字?
  • @luk2302 错字,但猜想这并不能阻止人们解决我的问题。 :)

标签: swift


【解决方案1】:

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

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

var _aClient:Client?

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

【讨论】:

  • 这很聪明!顺便说一句,我认为我们可能需要在get{} 中使用self._aClient。有时,如果 self. 丢失,Swift 编译器可以在某些情况下找到一个 var。
  • 我曾希望在 Swift 2 中有更好的东西。你真正想要的是一个带有只允许设置为 nil 的 setter 的 var,以及只在属性为 nil 时调用的 getter 代码。像 var aClient:Client { setnil; getnil { aClient = Client (ClientSession.shared ()) } } 如果在将变量设置为 nil 后获取变量,则会调用 getnil。
  • @gnasher729 Swift 3 万岁!请参阅我的答案:)
  • 很好的答案...这个方法很好地设置了我最初创建为 Lazy var 的 GMSCircle..谢谢!
  • 如果你使用 var _aClient:Client!那么您可以更轻松地在 getter 中配置 _aClient。
【解决方案2】:

因为the behavior of lazy changed in Swift 4,我写了一些structs,它们给出了非常具体的行为,在语言版本之间永远不应该改变。我把这些放在 GitHub 上,在 the BH-1-PD 许可下:https://github.com/RougeWare/Swift-Lazy-Patterns

ResettableLazy

这是与此问题相关的问题,它为您提供了一种延迟初始化值、缓存该值并销毁它以便以后可以延迟重新初始化的方法。

请注意,这需要 Swift 5.1! 对于 Swift 4 版本,请参阅 version 1.1.1 of that repo

this的简单用法非常直接:

@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's solution

以前,此答案取决于作为错误的行为。这个答案的旧版本、它的行为以及它为什么是一个错误都在 Swift bug SR-5172 的文本和 cmets 中进行了描述(截至 2017 年 7 月 14 日,PR #10,911 已解决),很明显,这行为从来都不是故意的。

该解决方案在那个 Swift 错误的文本中,也在 the history of this answer 中,但是因为它是一个在 Swift 3.2+ 中不起作用的漏洞利用我建议你这样做不要这样做那个。

【讨论】:

  • 恕我直言,这比公认的答案更简洁。
  • +1 用于! 的使用,但是,如果要使用此模式,则此变量的每个调用者仍必须解包Optional。例如,print(aClient) // prints "Optional(Clinet)\n"
  • 我今天早上刚在操场上试了一下,上传到这里:gist.github.com/pxpgraphics/4cfb7e02b6be7a583bf5f8a3ccbcd29a 所以也许这和字符串插值有关:]
  • 我想我不小心使用了这个漏洞利用...觉得很奇怪的是,长期存在的代码最近才开始导致崩溃(最近更新到 Swift 4)。您在编辑中链接到的解决方案与接受的解决方案相比是否有很大的优势?接受的解决方案似乎符合我的需求,您链接到的解决方案似乎过于复杂,我看不出它的优势。
  • @Ben Leggiero 我感谢您的认可 :) 虽然,正如我在回答中提到的,我对我的版本并不完全满意。如果你愿意分享,我很想看看你得到了什么?
【解决方案3】:

编辑:根据 Ben Leggiero 的回答,惰性变量在 Swift 3 中可以是 nilable。 编辑 2:似乎 nilable 懒惰变量不再存在。

非常迟到了,甚至不确定这是否与 Swift 3 相关,但到此为止。 David 的回答很好,但是如果你想创建许多惰性的 nil-able 变量,你将不得不编写一个相当大的代码块。我正在尝试创建一个封装这种行为的 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 的初始化程序扩展您想要使用它的每种类型。

如果有人想从这里捡起它,那就太棒了。

【讨论】:

  • 这基本上就是我现在在 Swift 4 中所做的事情
【解决方案4】:

这允许将属性设置为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
}

【讨论】:

    【解决方案5】:

    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)
    

    【讨论】:

    • 查看属性包装器替代方案。语法现在更简洁了。
    【解决方案6】:

    这里有一些很好的答案。
    在很多情况下确实需要重置惰性变量。

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

    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
    

    【讨论】:

      【解决方案7】:

      如果目标是重新初始化惰性属性,但不一定将其设置为 nil,来自 Phlippie Bosman 和 Ben Leggiero 的构建,这里有一些东西可以避免每次读取值时进行条件检查:

      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"
      

      【讨论】:

        【解决方案8】:

        我将@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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-03-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-05-15
          相关资源
          最近更新 更多