简体   繁体   English

如何在Swift中的变异结构上正确创建一个延迟派生属性?

[英]How to properly make a lazy derived property on a mutating struct in Swift?

I'm making a mutating struct with a really expensive-to-compute derived value. 我正在制作一个具有非常昂贵的计算导出值的变异结构。 So what I want to do is to compute this derived value lazily and store the result, until the struct gets mutated again, at which point the derived value is no longer valid and needs to be recomputed. 所以我想做的是懒惰地计算这个派生值并存储结果,直到结构再次发生变异,此时派生值不再有效,需要重新计算。

(Failed) Option 1: Generated property (失败)选项1:生成的属性

If the derived value is a generated property (as shown below), the correct value is always returned but is always recalculated. 如果派生值是生成的属性(如下所示),则始终返回正确的值,但始终重新计算。

(Failed) Option 2: Lazy-loaded property (失败)选项2:延迟加载的属性

If it is a lazy property instead, the calculation is only done once... ever. 如果它是一个懒惰的属性,那么计算只进行一次......永远。 So once the struct is mutated, the derived value is wrong and won't be recomputed. 因此,一旦结构发生变异,派生值就会出错,并且不会重新计算。 Also, I can't access the property if I assign a constant value from the struct. 此外,如果我从结构中分配一个常量值,我无法访问该属性。

Is there any possible solution in Swift 1.2 or do I need to file a radar? 在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

Using an embedded class gets around the limitations on mutating a struct. 使用嵌入式类可以解决变异结构的限制。 This lets you use a by‐value type that does not run expensive computations until they are needed, but still remembers the result afterward. 这允许您使用按值类型,该类型在需要之前不会运行昂贵的计算,但仍会记住之后的结果。

The example Number struct below computes and remembers its square property in a way that behaves just like you describe. 下面的示例Number结构以一种与您描述的方式相似的方式计算并记住其方形属性。 The math itself is ridiculously inefficient, but it is a simple way to illustrate the solution. 数学本身效率低得离谱,但这是解释解决方案的简单方法。

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

And this is how it performs: 这就是它的表现:

// 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!”

As a more realistic example, I have used this technique on date structs to make sure the non‐trivial computations for converting between calendar systems are run as little as possible. 作为一个更现实的例子,我在日期结构上使用了这种技术,以确保在日历系统之间进行转换的非平凡计算尽可能少地运行。

This is a really interesting question. 这是一个非常有趣的问题。 I have a few different ideas here that could help. 我在这里有一些不同的想法可以提供帮助。

First off, you are slightly misusing the idea of a lazy property. 首先,你有点滥用lazy财产的想法。 You can only have lazy stored properties because all that lazy does is delay execution until it is first executed. 您只能拥有延迟存储的属性,因为所有延迟都是延迟执行,直到它首次执行。 That value is then stored in the property from then on. 然后,该值将stored在属性中。 You are dealing with a computed property which cannot be used in that way. 您正在处理不能以这种方式使用的计算属性。 You can certainly file a radar, but I think it is a lost cause because your use case is not a valid lazy case IMO. 你当然可以提交雷达,但我认为这是一个失败的原因,因为你的用例不是一个有效的懒惰IMO。

With that said, I think you have a few options. 话虽如此,我认为你有几个选择。

Option 1 - Use a Class with Property Observers 选项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()
    }
}

The advantage here is that you can still lazily compute the derivedValue at the point that the property is called. 这里的优点是你仍然可以在调用属性时懒惰地计算derivedValue The downside is that you are no longer using a "by value" object. 缺点是您不再使用“按值”对象。

Option 2 - Compute Expensive Value in Mutate Method 选项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
    }
}

The advantage to this approach is that you can still keep your "by value" object, but you have to compute the expensive value at the time of mutation. 这种方法的优点是您仍然可以保留“按值”对象,但是您必须在突变时计算昂贵的值。 You can't do it inside the derivedValue property because you cannot mutate self inside a computed property for a struct. 您无法在derivedValue属性中执行此操作,因为您无法在结构的计算属性内变异self

Option 3 - Use Static Struct to Monitor Value Changes 选项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()
    }
}

This approach allows you to keep your "by value" object while also allowing you to lazily compute the expensive value. 这种方法允许您保留“按值”对象,同时还允许您懒洋洋地计算昂贵的值。 The major issue here though is that this will only work for a single object. 这里的主要问题是这只适用于单个对象。 If you are creating multiple objects, this is a bad approach. 如果要创建多个对象,这是一种不好的方法。

Summary 摘要

Unfortunately, this is not a valid use case for a lazy property. 不幸的是,这不是一个懒惰属性的有效用例。 However, there are other approaches to solving this problem. 但是,还有其他方法可以解决这个问题。 Hopefully one of these will be sufficient. 希望其中一个就足够了。 Based on all the info you have provided, I would venture a guess that Option 2 is probably your best bet. 根据您提供的所有信息,我冒昧地猜测选项2可能是您最好的选择。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM