如何在 Swift 中的变异结构上正确地创建惰性派生属性?

Posted

技术标签:

【中文标题】如何在 Swift 中的变异结构上正确地创建惰性派生属性?【英文标题】:How to properly make a lazy derived property on a mutating struct in Swift? 【发布时间】:2015-03-06 15:21:00 【问题描述】:

我正在制作一个具有非常昂贵的计算派生值的变异结构。所以我想做的是懒惰地计算这个派生值并存储结果,直到结构再次变异,此时派生值不再有效,需要重新计算。

(失败)选项 1:生成的属性

如果派生值是生成的属性(如下所示),则始终返回正确的值,但始终会重新计算。

(失败)选项 2:延迟加载属性

如果它是惰性属性,则计算只进行一次......永远。因此,一旦结构发生变异,派生值就是错误的,不会被重新计算。另外,如果我从结构中分配一个常量值,我将无法访问该属性。

在 Swift 1.2 中是否有任何可能的解决方案,或者我需要提交雷达文件吗?

struct Struct 
    var value: Int

    // Option 1: Generated property
    var derivedValue: Int 
        println("Doing expensive calculation")
        return self.value * 2
    

    // Option 2: Lazy property
    lazy var derivedValue: Int = 
        println("Doing expensive calculation")
        return self.value * 2
    ()

    init(value: Int) 
        self.value = value
    

    mutating func mutate() 
        value = random()
    


var test = Struct(value: 2)
test.derivedValue
test.derivedValue // If not lazy, expensive calculation is done again here
test.mutate()
test.derivedValue // If lazy, this has wrong value

let test2 = test
test2.derivedValue // Compiler error if using lazy implementation

【问题讨论】:

如果你也发布了你的结构的惰性版本会很有帮助。 现在添加了惰性实现 【参考方案1】:

使用嵌入式类可以绕过改变结构的限制。这使您可以使用按值类型,在需要计算之前不会运行昂贵的计算,但之后仍会记住结果。

下面的示例Number struct 以与您描述的一样的方式计算并记住它的 square 属性。数学本身的效率低得离谱,但它是一种说明解决方案的简单方法。

struct Number 

    // Store a cache in a nested class.
    // The struct only contains a reference to the class, not the class itself,
    // so the struct cannot prevent the class from mutating.
    private class Cache 
        var square: Int?
        var multiples: [Int: Int] = [:]
    
    private var cache = Cache()

    // Empty the cache whenever the struct mutates.
    var value: Int 
        willSet 
            cache = Cache()
        
    

    // Prevent Swift from generating an unwanted default initializer.
    // (i.e. init(cache: Number.Cache, value: Int))
    init(value: Int) 
        self.value = value
    

    var square: Int 
        // If the computed variable has been cached...
        if let result = cache.square 

            // ...return it.
            print("I’m glad I don’t have to do that again.")
            return result
         else 

            // Otherwise perform the expensive calculation...
            print("This is taking forever!")
            var result = 0
            for var i = 1; i <= value; ++i 
                result += value
            

            // ...store the result to the cache...
            cache.square = result

            // ...and return it.
                return result
        
    

    // A more complex example that caches the varying results
    // of performing an expensive operation on an input parameter.
    func multiple(coefficient: Int) -> Int 
        if let result = cache.multiples[coefficient] 
            return result
         else 

            var result = 0
            for var i = 1; i <= coefficient; ++i 
                result += value
            

            cache.multiples[coefficient] = result
                return result
        
    

这就是它的表现:

// The expensive calculation only happens once...
var number = Number(value: 1000)
let a = number.square // “This is taking forever!”
let b = number.square // “I’m glad I don’t have to do that again.”
let c = number.square // “I’m glad I don’t have to do that again.”

// Unless there has been a mutation since last time.
number.value = 10000
let d = number.square // “This is taking forever!”
let e = number.square // “I’m glad I don’t have to do that again.”

// The cache even persists across copies...
var anotherNumber = number
let f = anotherNumber.square // “I’m glad I don’t have to do that again.”

// ... until they mutate.
anotherNumber.value = 100
let g = anotherNumber.square // “This is taking forever!”

作为一个更实际的例子,我在日期结构上使用了这种技术,以确保在日历系统之间进行转换的重要计算尽可能少地运行。

【讨论】:

【参考方案2】:

这是一个非常有趣的问题。我在这里有一些不同的想法可能会有所帮助。

首先,您稍微误用了lazy 属性的概念。你只能拥有lazy stored properties,因为惰性所做的只是延迟执行,直到它第一次执行。从那时起,该值在属性中为 stored。您正在处理一个不能以这种方式使用的计算属性。您当然可以提交雷达,但我认为这是一个失败的原因,因为您的用例不是有效的惰性案例 IMO。

话虽如此,我认为您有几个选择。

选项 1 - 使用具有属性观察器的类

class Calculator 
    var value: Int 
        didSet 
            valueChanged = true
        
    

    var valueChanged = false

    var derivedValue: Int 
        if valueChanged 
            println("Doing expensive calculation")
            valueChanged = false
        

        return self.value * 2
    

    init(value: Int) 
        self.value = value
    

    func mutate() 
        value = random()
    

这里的优点是您仍然可以在调用属性时延迟计算derivedValue。缺点是您不再使用“按值”对象。

选项 2 - 在变异方法中计算昂贵的值

struct SortOfLazyCalculator 
    var value: Int
    var expensiveComputedValue: Int = 0 // just guessing
    var derivedValue: Int 
        return self.value * 2
    

    init(value: Int) 
        self.value = value
    

    mutating func mutate() 
        value = random()
        expensiveComputedValue = random() // not sure what the expensive calculation is
    

这种方法的优点是您仍然可以保留“按值”对象,但您必须在突变时计算昂贵的值。您不能在 derivedValue 属性内执行此操作,因为您不能在结构的计算属性内更改 self

选项 3 - 使用静态结构监控值变化

struct Struct 
    var value: Int
    var derivedValue: Int 
        struct Static  static var previousValue: Int? 

        if Static.previousValue == nil 
            println("Setting previous value since it is nil")
            Static.previousValue = value
        

        if value != Static.previousValue! 
            println("Doing expensive calculation")
            Static.previousValue = value
        

        return self.value * 2
    

    init(value: Int) 
        self.value = value
    

    mutating func mutate() 
        value = random()
    

这种方法允许您保留“按值”对象,同时还允许您懒惰地计算昂贵的值。但这里的主要问题是这仅适用于单个对象。如果您要创建多个对象,这是一种不好的方法。

总结

不幸的是,这不是惰性属性的有效用例。但是,还有其他方法可以解决这个问题。希望其中之一就足够了。根据您提供的所有信息,我大胆猜测选项 2 可能是您最好的选择。

【讨论】:

感谢您的详尽回复。一个惰性存储的属性正是我想要的。由于该操作的计算成本很高,因此我想延迟执行并在计算后存储它。我只是希望它在结构发生突变时失效并返回其“惰性”状态。我同意 Option 2 最适合保持不变性并且仍然具有没有任何静态内容的值类型。问题是该属性不再是懒惰的了。因此,如果我创建结构,对其运行一系列不同的突变,然后使用派生值,则将为每个突变重新计算该值。 我完全明白你想要做什么,但同样,你在滥用 lazy 属性。 lazy 属性仅在初始化之前是惰性的,这不是您想要做的。所以当你说你想要一个惰性属性时,我认为你绝对不想要一个惰性属性,因为这不是它们的工作方式。由于结构类型的不可变性,您只能在可变函数中更新昂贵计算的值。 是的,目前lazy 关键字根本不适用于值类型。我想我会归档雷达。 酷哥们,祝你好运。如果您认为答案足够,您可以将其标记为足够吗?干杯。

以上是关于如何在 Swift 中的变异结构上正确地创建惰性派生属性?的主要内容,如果未能解决你的问题,请参考以下文章

如何通过 Swift 中的闭包设置惰性计算属性的值?

Swift 中的惰性只读属性

闭包中的Swift可变结构和结构的行为不同

解析器中的 AppSync GraphQL 变异服务器逻辑

在 Swift 3.0 中的转义闭包中改变自我(结构/枚举)

如何在Swift中正确地从父节点中删除SKSpriteNode的子类(要从数组中移除,屏幕上的空格......)?