类型属性
实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。
也可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性
想象一下,你正在构建一个多层次的游戏。每个级别都有一些属性或存储属性:
struct Level {
let id: Int
var boss: String
var unlocked: Bool
}
let level1 = Level(id: 1, boss: "Chameleon", unlocked: true)
let level2 = Level(id: 2, boss: "Squid", unlocked: false)
let level3 = Level(id: 3, boss: "Chupacabra", unlocked: false)
let level4 = Level(id: 4, boss: "Yeti", unlocked: false)
你可以使用类型属性来存储游戏的进度,因为玩家可以解锁每个级别。类型属性使用静态声明:
struct Level {
static var highestLevel = 1
let id: Int
var boss: String
var unlocked: Bool
}
这里,highestLevel是级别本身的属性,而不是实例。这意味着你不能在一个实例上访问这个属性:
// Error: you can't access a type property on an instance
let highestLevel = level3.highestLevel
相反,你可以通过类型本身访问它:
let highestLevel = Level.highestLevel // 1
使用类型属性意味着你可以从应用程序或算法代码中任何地方检索相同的存储属性值。游戏的进度可以从游戏中的任何级别或任何其他地方访问,比如主菜单。
属性观察器
对于你的级别实现,当玩家解锁一个新级别时,自动设置highestLevel是很有用的。为此,你需要一种方法来监听属性更改。值得庆幸的是,有一些属性观察器在属性更改之前和之后被调用。
当一个属性将要被更改时,将调用一个willSet observer,在属性更改后调用didSet observer。它们的语法类似于getter和setter:
struct Level {
static var highestLevel = 1
let id: Int
var boss: String
var unlocked: Bool {
didSet {
if unlocked && id > Level.highestLevel {
Level.highestLevel = id
}
}
}
}
现在,当玩家解锁新级别时,如果这个级别是新的高度,它将更新highestLevel类型的属性。这里有几点需要注意:
• 你可以从didSet观察器中访问值。记住didSet在值被设置后调用。
• 即使在类型的实例中,仍然需要使用类型名称前缀访问类型属性。你需要使用全名Level.highestLevel,而不仅仅是highestLevel,表示你正在访问一个类型属性。
willSet和didSet观察者只能用于存储属性。如果您想要监听计算属性的更改,只需将相关代码添加到属性的setter中。
同时,请记住,在初始化期间设置属性时,不会调用willSet和didSet观察器;当你为一个完全初始化的实例分配一个新值时,它们才会被调用。这意味着属性观察器只对变量属性有用,因为常量属性只在初始化期间设置。
限制一个变量
你还可以使用属性观察器来限制变量的值。假设你有一个灯泡,它只能支持通过它的灯丝的最大电流。
struct LightBulb {
static let maxCurrent = 40
var current = 0 {
didSet {
if current > LightBulb.maxCurrent {
print("Current too high, falling back to previous setting.")
current = oldValue
}
}
}
}
在这个例子中,如果当前流入的灯泡超过了最大值,它将恢复到它最后的成功值。注意,didSet中有一个有用的oldValue常量,所以你可以访问以前的值。
试一试:
var light = LightBulb()
light.current = 50
var current = light.current // 0
light.current = 40
current = light.current // 40
你试着将灯泡设置为50安培,但是灯泡拒绝了这个输入。很好!
懒属性
如果你有一个可能需要一些时间来计算的属性,那么在需要该属性之前,你不想要降低它的速度。这就需要惰存储属性。这对于下载用户的个人资料、图片或进行复杂计算是很有用的。
看看这个Circle结构的例子,它使用pi计算周长:
struct Circle {
lazy var pi = {
return ((4.0 * atan(1.0 / 5.0)) - atan(1.0 / 239.0)) * 4.0
}()
var radius = 0.0
var circumference: Double {
mutating get {
return pi * radius * 2
}
}
init(radius: Double) {
self.radius = radius
}
}
这里,你不信任标准库中可用的pi值;你想自己计算一下。
你可以使用它的初始化器创建一个新的圆,并且pi计算还不会运行:
var circle = Circle(radius: 5) // got a circle, pi has not been run
pi的计算要耐心等待,直到你需要它。只有当你要计算圆周时,pi计算并赋值。
let circumference = circle.circumference // 31.42
// also, pi now has a value
你已经注意到pi使用{}()模式来计算它的值,即使它是一个存储的属性。后面的括号将立即执行花括号内的代码。但是由于pi被标记为lazy,所以这个计算被推迟到你第一次访问该属性。
相比较,circumference是一个计算属性,因此每次访问它时都要计算它。如果半径变化,周长的值会改变。pi作为一个惰存储属性,只是第一次计算。这很好,因为谁想要一次又一次地计算相同的东西?
lazy属性必须是一个变量,用var来定义,而不是用let定义一个常量。当你第一次初始化结构时,属性实际上没有任何值。然后,当代码的某些部分请求该属性时,它的值将被计算出来。所以即使这个值只改变一次,你仍然需要使用var。
下面是代码的两个高级特性:
•由于pi的值变化,circumference getter必须标记为mutating。这改变了结构的值。
•由于pi是结构的一个存储属性,所以你需要一个自定义初始化器,初始化时只使用radius。因为,结构的自动初始化器包括所有的存储属性。
现在不要太担心那些高级功能。在下一章中,你将了解到更多关于可变关键字和自定义初始化器的知识。最重要的是让你的思想集中起来,那就是懒储存属性是如何运作的。其余的细节都是橱窗装饰。
关键点
•属性是变量和常量,并且是命名类型的一部分。
•存储属性分配内存以存储值。
•计算属性是每次代码请求去计算的属性,不是存储值到内存中。
•静态关键字标记一个类型属性,该属性适用于特定类型的所有实例。
•lazy关键字可以防止存储属性的值被计算,直到代码第一次使用它。当属性的初始值是计算密集型的,或者在初始化对象之后才知道属性的初始值时,就需要使用惰初始化。