Как правильно сделать ленивые получена собственность на изменения структуры в Swift?


Я создаю мутирующую структуру с очень дорогим для вычисления производным значением. Поэтому я хочу лениво вычислить это производное значение и хранить результат до тех пор, пока структура снова не мутирует, и в этот момент производное значение больше не является допустимым и нуждается в повторном вычислении.

(ошибка) Вариант 1: генерируемое СВОЙСТВО

Если производное значение является сгенерированным свойством (как показано ниже), правильное значение всегда возвращается, но всегда пересчитывается.

(не удалось) Вариант 2: свойство Lazy-loaded

Если это свойство является ленивым, то вычисление выполняется только один раз... когда-либо. Таким образом, как только структура мутирует, полученное значение неверно и не будет пересчитано. Кроме того, я не могу получить доступ к свойству, если я назначаю постоянное значение из структуры.

Есть ли какое-либо возможное решение в 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 6

2 ответа:

Использование встроенного класса позволяет обойти ограничения на изменение структуры. Это позволяет использовать тип by-value, который не выполняет дорогостоящие вычисления до тех пор, пока они не понадобятся, но все равно запоминает результат позже.

Приведенный ниже пример структуры Number вычисляет и запоминает свое квадратное свойство таким образом, что ведет себя точно так же, как вы описываете. Сама математика смехотворно неэффективна, но это простой способ проиллюстрировать решение.

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!”
В качестве более реалистичного примера я использовал этот метод на структурах дат, чтобы убедиться, что нетривиальные вычисления для преобразования между календарными системами выполняются как можно меньше.

Это действительно интересный вопрос. У меня есть несколько разных идей, которые могут помочь.

Во-первых, вы слегка злоупотребляете идеей свойства lazy. Вы можете иметь только lazy сохраненные свойства, потому что все, что делает lazy, это задерживает выполнение до тех пор, пока оно не будет выполнено первым. После этого это значение будет 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-вычислить дорогое значение в методе Mutate

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, вероятно, ваш лучший выбор.