在这种情况下,使用常数值作为 "nil"(没有值)是一种好的做法吗?

Is using a constant value as a "nil" (absence of value) a good practice in this case?

我正在学习 Swift,但我的一个模型 类 遇到了问题。 我想要做的是拥有一个延迟加载的 属性,当它基于的数据发生变化时,它可以是 "invalidated"。 (像这样:)

我现在的情况是这样的:

class DataSet {
    var entries: [Entry]

    var average: Double? {
        return self.entries.average
    }
}

Array<Entry>.average 计算 Entry 的 属性 的平均值,但如果数组为空,则 returns nil .

由于这个平均值的计算成本可能很高,我想 lazily存储它的缓存值并仅在必要时重新计算它(当 DataSet.entries 被修改时)。

现在,在 this answer 之后,我应该这样做:

class DataSet {
    var entries: [Entry]
    var _average: Double?
    var average: Double? {
        if _average == nil {
            _average = self.entries.average
        }
        return _average
    }
}

但是我在处理 平均值要重新计算 以及 数组为空 和return.

没有有意义的平均值

因为我知道平均值总是正数,所以我可以使用默认值(例如-1.0)来指示缓存不再有效,并且nil 表示没有条目,反之亦然(nil 表示必须再次计算平均值;-1.0 表示没有条目)。

然而,这似乎并不优雅,或者 "Swift way" 实现此行为(或者确实如此?)。

我该怎么办?

使用零

首先,永远不要使用类型域的特定值来表示值的缺失。换句话说,不要使用负数表示没有值。 nil 就是这里的答案。

所以 average 应该声明为 Double?.

条目确实发生变化时清除缓存

接下来您需要在每次 entries 发生变异时清除您的缓存。您可以为此使用 didSet

class DataSet {
    private var entries: [Entry] = [] {
        didSet { cachedAverage = nil }
    }

    private var cachedAverage: Double?
    var average: Double? {
        if cachedAverage == nil {
            cachedAverage = self.entries.average
        }
        return cachedAverage
    }
}

条目为空时

最后,如果您认为空数组的 average 应该是 nil,那么为什么不相应地更改您添加到的 average 计算的 属性 SequenceType?

您可以使用 didSet:

class DataSet {
    var entries: [Entry] {
        didSet {
            /// just mark the average as outdated, it will be
            /// recomputed when someone asks again for it
            averageOutOfDate = true
        }
    }

    /// tells us if we should recompute, by default true
    var averageOutOfDate = true
    /// cached value that avoid the expensive computation
    var cachedAverage: Double? = nil

    var average: Double? {
        if averageOutOfDate {
            cachedAverage = self.entries.average
            averageOutOfDate = false
        }
        return cachedAverage
    }
}

基本上,每当 entries 属性 值发生变化时,您将缓存值标记为已过时,并使用此标志来了解何时重新计算它。

是一个很好的具体解决方案。但是,我建议采用不同的方法。这是我会做的:

我会制定一个协议来定义所有需要从外部访问的重要位。

然后我会让 2 structs/classes 符合这个协议。

第一个 struct/class 将是缓存层。

第二个 struct/class 将是实际的实现,它只会被缓存层访问。

缓存层将有一个实际实现的私有实例 struct/class,并且会有一个变量,例如 "isCacheValid",所有变异操作都会将其设置为 false使基础数据无效(并扩展为计算值,例如平均值)。

这种设计使得实际实现 struct/class 相当简单,并且完全与缓存无关。

缓存层执行所有缓存任务,完全不知道如何计算缓存值的细节(因为它们的计算只是委托给实现 class。

您绝对不应该使用魔法值,例如 -1 来指示某种状态。但我同意你不应该使用 nil 来表示 "the cached value is invalidated and must be recalculated" 和 "the cached average has been calculated and is nil because there were zero Entry objects"。其他建议仅将计算值设置为 nil 的解决方案的问题是它无法区分 "invalidated" 和 "calculated but nil" 两种状态,可能会调用 entries.average即使您可能已经这样做了。不可否认,这在计算上可能并不昂贵,但它混淆了两个截然不同的状态。

一个 Swift 解决方案是 enum:

class DataSet {
    private enum CachedValue {
        case Calculated(Double?)
        case Invalidated
    }

    var entries = [Entry]() {
        didSet {
            cachedAverage = .Invalidated
        }
    }

    private var cachedAverage: CachedValue = .Invalidated

    var average: Double? {
        switch cachedAverage {
        case .Calculated(let result):
            return result
        case .Invalidated:
            let result = entries.average
            cachedAverage = .Calculated(result)
            return result
        }
    }
}

这会捕获 .Invalidated.Calculated 之间的不同状态,并根据需要延迟重新计算。