【问题标题】:Swift memoizing/caching lazy variable in a struct在结构中快速记忆/缓存惰性变量
【发布时间】:2019-01-14 18:16:35
【问题描述】:

我在 Swift 中喝了 struct/value koolaid。现在我有一个有趣的问题,我不知道如何解决。我有一个结构,它是一个容器,例如

struct Foo {
    var bars:[Bar]
}

在对此进行编辑时,我会创建副本以便保留撤消堆栈。到目前为止,一切都很好。就像好的教程显示的那样。不过,我对这个人使用了一些派生属性:

struct Foo {
    var bars:[Bar]

    var derivedValue:Int {
        ...
    }
}

在最近的分析中,我注意到 a) 计算 derivedValue 的计算有点昂贵/冗余 b) 在各种用例中并不总是需要计算。

以我经典的 OOP 方式,我会将其设为记忆/惰性变量。基本上,在调用之前让它为零,计算一次并存储它,并在以后的调用中返回所述结果。由于我遵循“制作副本以进行编辑”模式,因此不会破坏不变量。

但是如果它是结构,我不知道如何应用这种模式。我可以这样做:

struct Foo {
    var bars:[Bar]
    lazy var derivedValue:Int = self.computeDerivation()
}

这有效,直到结构引用该值本身,例如

struct Foo {
    var bars:[Bar]
    lazy var derivedValue:Int = self.computeDerivation()

    fun anotherDerivedComputation() {
        return self.derivedValue / 2
    }
}

此时,编译器会报错,因为anotherDerivedComputation 导致接收器发生更改,因此需要标记为mutating。将访问器标记为变异是错误的。但是为了咧嘴笑,我尝试了一下,但这会产生一系列新的问题。现在任何我有类似表达的地方

XCTAssertEqaul(foo.anotherDerivedComputation(), 20)

编译器会抱怨,因为参数隐式是非可变的 let 值,而不是 var。

我是否缺少一种模式,因为我有一个带有延迟/延迟/缓存成员的结构?

【问题讨论】:

    标签: swift memoization swift-structs


    【解决方案1】:

    我将问题概括为一个更简单的问题:一个 x,y Point 结构,它想要延迟计算/缓存 r(adius) 的值。我在块闭包周围使用了 ref 包装器,并提出了以下建议。我称之为“Once”块。

    import Foundation
    
    class Once<Input,Output> {
        let block:(Input)->Output
        private var cache:Output? = nil
    
        init(_ block:@escaping (Input)->Output) {
            self.block = block
        }
    
        func once(_ input:Input) -> Output {
            if self.cache == nil {
                self.cache = self.block(input)
            }
            return self.cache!
        }
    }
    
    struct Point {
        let x:Float
        let y:Float
        private let rOnce:Once<Point,Float> = Once {myself in myself.computeRadius()}
    
        init(x:Float, y:Float) {
            self.x = x
            self.y = y
        }
    
        var r:Float {
            return self.rOnce.once(self)
        }
    
        func computeRadius() -> Float {
            return sqrtf((self.x * self.x) + (self.y * self.y))
        }
    }
    
    let p = Point(x: 30, y: 40)
    
    print("p.r \(p.r)")
    

    我选择让 OnceBlock 接受输入,否则将其初始化为引用 self 的函数会很痛苦,因为 self 在初始化时还不存在,因此推迟该链接更容易到缓存/调用站点(var r:Float

    【讨论】:

    • 既有趣又实用!
    【解决方案2】:

    我知道使这项工作的唯一方法是将惰性成员包装在一个类中。这样,包含对对象的引用的结构可以保持不可变,而对象本身可以被改变。

    几年前我写了一篇关于这个主题的博客文章:Lazy Properties in Structs。它详细介绍了细节,并提出了两种不同的包装类设计方法,具体取决于惰性成员是否需要结构中的实例信息来计算缓存值。

    【讨论】:

      【解决方案3】:

      记忆不会发生在结构内部。记忆的方法是将字典存储在某个单独的空间中。关键是推导该值的任何内容,而该值就是该值,计算一次。您可以将其设为结构类型的静态,就像对其命名空间一样。

      struct S {
          static var memo = [Int:Int]()
          var i : Int
          var square : Int {
              if let result = S.memo[i] {return result}
              print("calculating")
              let newresult = i*i // pretend that's expensive
              S.memo[i] = newresult
              return newresult
          }
      }
      
      var s = S(i:2)
      s.square // calculating
      s = S(i:2)
      s.square // [nothing]
      s = S(i:3)
      s.square // calculating
      

      【讨论】:

      • 虽然字典样式缓存通常是“记忆化”的常见实现,但我不认为该术语明确限于上述实现。维基百科指出“一个记忆函数“记住”与一组特定输入相对应的结果。但也许,这个词具有误导性,“缓存”在标题中会更好吗?
      • 好吧,让我换一种说法。以我建议的方式记忆会解决问题吗?
      • 添加了我建议的示例。
      • 可以的。底线似乎是您必须在某处“装箱”可变值(无论是在“缓存属性”框中,还是在备用表中)。
      • 那么谁能在这里得到答案呢?您和@OleBegemann 的回答共同引导我朝着正确的方向前进。而且你们俩的分数都疯了。 :)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-26
      • 2021-11-24
      • 1970-01-01
      • 2014-12-03
      • 2016-06-27
      • 2012-06-17
      相关资源
      最近更新 更多