在这种情况下,使用常数值作为 "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
之间的不同状态,并根据需要延迟重新计算。
我正在学习 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
之间的不同状态,并根据需要延迟重新计算。