在 Swift 中,可以使用 lazy
惰性属性来延迟变量或常量的初始化,直到第一次使用它。这在其初始值计算成本较高或非立即需要时很有用。
一般声明的变量,不管后面用不用,都会在一开始初始化,这样会占用存储;但如果不声明该变量,改用函数方法返回,则每次使用时都要计算一次该方法,这样会影响性能。所以惰性属性 lazy
是一种中间地带:它们仅在需要使用时计算一次,您只需支付一次性能成本;如果它们从未使用过,则代码永远不会运行。这是双赢的!
在Swift中,结构体或类的惰性属性是指:只有在首次访问时才会被计算和初始化的储存属性。这对于需要延迟初始化的属性非常有用,可以节省内存和性能。惰性属性可以通过 = 等号来辨认,这点和储存属性类似。
惰性属性的具体写法:
lazy
惰性属性时需要显式声明类型=
等号,然后后面跟着一个闭包在下面示例中,Circle
定义了一个惰性属性 diameter
,它的值是根据 radius
计算得出的圆的直径。
diameter
**属性时,会执行惰性属性的计算和初始化,打印"Calculating diameter"diameter
**属性时,不会重新计算和初始化,直接返回之前计算好的值print(myCircle.diameter)
而不是 print(myCircle.diameter())
来访问该值// 以下是一个示例代码,演示了如何在结构体中使用惰性属性:
struct Circle {
let radius: Double
// 惰性属性,diameter 它需要在首次访问时才初始化,而不是在“类或结构初始化”时就初始化
lazy var diameter: Double = {
print("Calculating diameter")
// Expensive computation to create array
return self.radius * 2
}()
}
var myCircle = Circle(radius: 5.0)
print(myCircle.diameter) // 第一次访问,会计算和初始化惰性属性
print(myCircle.diameter) // 后续访问,不会重新计算和初始化
在 Swift 中 Lazy
关键字主要用于储存属性,而不适用于计算属性。因为计算属性的值是动态计算的,且每次访问时都会重新计算,因此不需要使用 Lazy
关键字。计算属性可以通过 :
冒号辨认。
<aside> 💡
惰性属性 与 计算属性 最大的不同是: 惰性属性:只有在首次访问时才会被计算和初始化,之后惰性属性会存储计算的结果,因此后续的访问就不需要重新计算了 计算属性:每次访问都要重新计算,和方法调用一样
</aside>
// 注意惰性属性的等号 = 和闭包后面的括号(),它是要在访问时,运行闭包返回一个值,再赋值给属性
lazy var diameter: Double = {
print("Calculating diameter")
return self.radius * 2
}()
// 而计算属性没有等号 = ,也没有闭包后面的括号,它是每次调用的时候都重新计算一遍
var exampleStringValue: String {
get {
return exampleString
}
set {
exampleString = newValue
}
}
使用 lazy
时常见的抱怨是它们会使代码变得混乱:因为 lazy
属性没有将属性和方法分离,而更像是属性和功能混合在一起的灰色区域。有一个简单的解决方案,可以创建一个方法,将你的惰性属性与它们所依赖的代码分开。如果你想使用这种方法,最好将创建的这个独立方法标记为 private
,这样它就不会被意外使用。
class Singer {
let name: String
init(name: String) {
self.name = name
}
// 声明惰性属性,其值是从一个私有方法中返回的
lazy var reversedName: String = self.getReversedName()
// 把功能逻辑放到方法中,这样 惰性属性 就和 逻辑方法 分离开了
private func getReversedName() -> String {
return "\\(self.name.uppercaseString) backwards is \\
(String(self.name.uppercaseString.characters.reverse()))!"
}
}
let taylor = Singer(name: "Taylor Swift")
print(taylor.reversedName)
关于单例模式的说明详情见这里:Swift 实现单例模式
例如我们要创建一个 Singer
类,里面包含一个属性是 MusicPlayer
类。该属性需要是单例模式,因为不管我们创建多少个 Singer
,我们希望它们播放歌曲的时候,都是通过同一个 MusicPlayer
进行播放,这样才不会出现多个声音重叠的情况。
// 这是 MusicPlayer 类的代码,它在创建出来时会打印一条消息
class MusicPlayer {
init() {
print("Ready to play songs!")
}
}
// 这是 Singer 类的代码,它在创建出来时也会打印一条消息
class Singer {
init() {
print("Creating a new singer")
}
}
那如果我们想给 Singer
类添加一个 MusicPlayer
单例属性,我们只需要在 Singer
类中添加一行代码:
class Singer {
static let musicPlayer = MusicPlayer()
init() {
print("Creating a new singer")
}
}
let taylor = Singer()
// static 部分意味着此属性是类的静态属性,在类中可以共享访问,而不是在类的实例中
// 这意味着是通过 Singer.musicPlayer 访问,而不是 taylor.musicPlayer 访问
// let 部分当然意味着它是一个常数,引用不会发生变化
你可能想知道这一切与惰性属性有什么关系?运行这段代码可以发现:输出的是 “Creating a new singer” ,而 “Ready to play songs!” 消息并没有打印出来。除非你再添加一条命令 Singer.musicPlayer
,才会打印该消息。
所以结论是:Swift 所有的 static let
单例都是自动惰性的 —— 它们只在需要时被创建。这很容易做到,但也非常高效。
惰性序列 Lazy Sequences 是 Swift 中的一种性能优化技术,类似于惰性属性。它允许我们延迟计算序列中的元素,直到真正需要使用时才进行计算。这对于处理大型或无限序列特别有用。
例如经典的斐波那契数列递归算法(它递归不断往下拆,直到拆到 0 或 1),如果不用惰性序列,它是这样的:
fibonacci
函数,用递归方式计算斐波那契数列(0...20)
创建了一个从 0 到 20 的范围.map(fibonacci)
将 fibonacci
函数应用到这个范围的每个数字上,这会立即计算整个序列(0 到 20 步的所有斐波那契数)print(fibonacciSequence[10])
打印第 11 个斐波那契数(因为索引从 0 开始)func fibonacci(num: Int) -> Int {
if num < 2 {
return num
} else {
return fibonacci(num - 1) + fibonacci(num - 2)
}
}
let fibonacciSequence = (0...20).map(fibonacci)
print(fibonacciSequence[10])
由于 fibonacci
是某种成本很高的运算,所以可改用惰性序列:
let fibonacciSequence = (0...199).lazy.map(fibonacci)
print(fibonacciSequence[19])
(0...199)
创建了一个更大的范围,从0到199.lazy
属性,将这个序列转换为惰性序列.map(fibonacci)
定义了要应用到每个元素的操作,但不会立即执行print(fibonacciSequence[19])
只会计算到第20个斐波那契数(索引19),而不是一次计算全部 200 个以上代码会遇到错误,是因为惰性序列的工作方式有些特殊。当对一个惰性序列使用下标访问(如 fibonacciSequenceA[19]
)时,Swift 无法直接提供该功能;因为惰性序列是按需计算的,它并没有预先计算好所有的元素,因此不支持直接的下标访问。以下是可行的修改方法:
使用 dropFirst()
和 first
来获取特定位置的元素:
// 去掉前面 19 个元素,再取第一个元素(那就是原来的20位)
let element = fibonacciSequence.dropFirst(19).first!
print(element)
使用 enumerated()
和 first(where:)
来找到特定索引的元素:
let element = fibonacciSequence.enumerated().first(where: { $0.offset == 19 })!.element
print(element)
这些方法保持了惰性计算的特性,只计算到需要的元素为止。通过这些修改,你应该能够成功访问惰性序列中的特定元素了。记住,惰性序列的设计目的是为了提高大型数据集处理的效率,所以在使用时要考虑到它的特性和局限性。
// 修改后的完整代码:
func fibonacci(_ num: Int) -> Int {
if num < 2 {
return num
} else {
return fibonacci(num - 1) + fibonacci(num - 2)
}
}
let fibonacciSequenceA = (0...199).lazy.map(fibonacci)
let element = fibonacciSequenceA.dropFirst(19).first!
print(element)
// 使用 `!` 强制解包假定序列中一定有足够的元素。在实际应用中,你可能需要添加错误处理逻辑
然而,惰性序列也有其局限性,这在下面的代码中表现出来:
let fibonacciSequence = (0...199).lazy.map(fibonacci)
print(fibonacciSequence[19])
print(fibonacciSequence[19])
print(fibonacciSequence[19])
<aside> 💡
以上代码在每次访问 fibonacciSequence[19]
时,都会重新计算第20个斐波那契数,这是因为惰性序列不会缓存结果。因此惰性序列的局限性表现在缺乏记忆:每次访问同一元素都会重新计算,不会缓存结果。因此对于重复访问同一元素的场景,最好使用普通序列或实现自定义的缓存机制。鉴于惰性序列在多次访问同一元素时,性能可能比普通序列更慢。因此使用时要权衡计算延迟与潜在的重复计算成本。
</aside>