5

我在 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。

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

4

3 回答 3

3

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

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
于 2018-08-07T15:52:22.273 回答
2

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

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

于 2018-08-07T15:50:50.660 回答
2

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

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 在初始化时还不存在,因此将链接延迟到缓存更容易/呼叫站点 (the var r:Float)

于 2018-08-07T19:47:59.067 回答