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

时间:2015-03-06 15:21:01

标签: swift struct lazy-evaluation mutable

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

(失败)选项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

2 个答案:

答案 0 :(得分:4)

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

下面的示例Number结构以一种与您描述的行为相似的方式计算并记住其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!”

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

答案 1 :(得分:3)

这是一个非常有趣的问题。我在这里有一些不同的想法可以提供帮助。

首先,你有点滥用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 可能是您最好的选择。