Swift4.2 属性

属性 可以将值与特定类、结构体或者枚举类型相关联。存储属性将常量和变量值存储为实例的一部分,而计算属性则是通过计算得到一个值(而不是存储值)。计算属性适用于类、结构体和枚举类型,而存储属性只适用于类和结构体类型。

存储和计算属性通常都与特定类型的实例相关联。但是,属性也可以与类型本身相关联,这种属性称为类型属性。

此外,你还可以定义属性观察器以监视属性值的更改,并使用自定义的操作对其进行响应。你既可以将属性观察器添加到自己定义的存储属性中,也可以添加到从父类继承来的属性中。

存储属性

简单来说,存储属性是一个存储在特定的类或结构体中的常量或变量。存储属性可以是 变量存储属性 (由 var 关键字定义)或 常量存储属性 (由 let 关键字定义)。

你可以为存储属性提供默认值作为其定义的一部分,如 默认属性值 中所述。你还可以在初始化期间设置和修改存储属性的初始值。即使对于常量存储属性也是如此,如 在初始化期间给常量属性赋值 中所述。

下面的示例定义了一个名为 FixedLengthRange 的结构体,该结构体描述了一个整数范围,其范围长度在创建后无法更改:

1
2
3
4
5
6
7
8
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 该整数范围表示整数值 0、1 和 2
rangeOfThreeItems.firstValue = 6
// 该整数范围现在表示整数值 6、7 和 8

FixedLengthRange 的实例有一个名为 firstValue 的变量存储属性和一个名为 length 的常量存储属性。在上面的示例中,length 在创建新实例时被初始化,之后无法更改,因为它是常量属性。

常量结构体实例的存储属性

如果创建结构体实例并将该实例声明为常量,则无法修改实例的属性,即使它们被声明为变量属性:

1
2
3
4
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 此整数范围表示整数值 0、1、2 和 3
rangeOfFourItems.firstValue = 6
// 这行代码会报错,即使 firstValue 是一个变量属性

因为 rangeOfFourItems 被声明为常量(使用 let 关键字),所以即使 firstValue 是一个变量属性,也不可以改变它的值。

这种行为是由于结构体是 值类型 。当值类型的实例声明为常量时,其所有属性也都会被标记为常量。

类的行为却并非如此,这是因为类是 引用类型 。如果将引用类型的实例声明为常量时,你仍可以修改该实例的变量属性。

延迟存储属性

延迟存储属性 的初始值直到第一次使用时才进行计算。你可以通过在其声明之前标注 lazy 修饰符来表示一个延迟存储属性。

注释
你必须始终将延迟属性声明为变量(使用 var 关键字),因为延迟属性的初始值可能在实例初始化完成之后,仍然没有被赋值。而常量属性必须在实例初始化完成 之前 就获得一个值,因此不能声明为延迟。

当属性的初始值依赖于外部因素时,延迟属性就非常有用了,因为这些外部因素的值可能在实例初始化完成之后才知道。当属性的初始值需要执行复杂或代价高昂的计算时,你应该只在需要的时候才执行,这时候延迟属性也很有用。

下面的示例,使用延迟存储属性来避免复杂类的不必要的初始化。这个例子定义了两个名为 DataImporterDataManager 的类,它们的代码都没有全部列出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class DataImporter {
/*
DataImporter 是一个从外部文件导入数据的类。
假设该类需要花费大量时间来初始化。
*/
var filename = "data.txt"
// DataImporter 类将在此处提供数据导入功能
}

class DataManager {
lazy var importer = DataImporter()
var data = [String]()
// DataManager 类将在此处提供数据管理功能
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// 类型为 DataImporter 的 importer 属性实例尚未创建

DataManager 类有一个名为 data 的存储属性,它使用一个空的 String 数组进行初始化。尽管它的其余功能没有展示出来,但我们仍然可以猜到这个 DataManager 类的目的是管理并提供访问 String 数组的方法。

DataManager 类同时还具有从文件导入数据的功能。这个功能由 DataImporter 类提供,我们同时假定初始化 DataImporter 类需要大量时间。这可能是因为初始化 DataImporter 实例时,需要打开文件并将其内容读入内存。

因为 DataManager 实例管理数据的功能,并不依赖从文件导入数据的功能,因此在创建 DataManager 实例时,我们不应该立刻创建 DataImporter 实例。相反,在 DataImporter 第一次被使用时再创建它才更有意义。

因为 importer 属性被 lazy 修饰符所标记,因此它在第一次被访问时 DataImporter 实例才会被创建,例如当查询其 filename 属性时:

1
2
3
print(manager.importer.filename)
// 现在 DataImporter 的实例 importer 已经被创建了
// 打印 "data.txt"

注释
如果被 lazy 修饰符所标记的属性,同时被多个线程访问,并且该属性尚未被初始化,则无法保证该属性仅被初始化一次。

存储属性和实例变量

如果你有使用 Objective-C 的经验,你可能知道它提供了 两种 方法来存储值和引用。除了属性之外,你还可以使用实例变量来作为属性的底层存储。

Swift 将这些概念统一到一个属性声明中。Swift 属性没有相应的实例变量,并且属性的底层存储不能被直接访问。这种方式避免了在不同的上下文中如何访问值的混淆,并将属性的声明简化为单个明确的语句。有关属性的所有信息(包括其名称、类型和内存管理特征)都在单个位置定义,作为类型定义的一部分。

计算属性

除了存储属性之外,类、结构体和枚举还可以定义 计算属性 ,它们实际上并不存储值。相反,它们会提供了一个 getter 方法和一个可选的 setter 方法来间接读取和设置其他属性和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// 打印 "square.origin is now at (10.0, 10.0)"

此示例定义了三种用于处理几何形状的结构体:

  • Point 封装了一个点的 x 坐标和 y 坐标。
  • Size 封装了 widthheight
  • Rect 用原点和大小定义一个矩形。

Rect 结构体还提供了一个名为 center 的计算属性。Rect 的当前中心位置始终可以从其 originsize 来唯一确定,因此你并不需要将中心点存储为一个 Point 类型的存储属性。相反,我们应该在 Rect 中定义一个名为 center 的计算变量,并自定义它的 gettersetter 方法,这样我们就可以使用矩形的 center 属性,就像它是一个真正的存储属性一样。

上面的例子创建了一个名为 squareRect 变量。 square 变量原点初始化为 (0, 0),宽度和高度初始化为 10。该正方形由下图中的蓝色方块表示。

然后通过点语法( square.center )访问 square 变量的 center 属性,这会导致调用 centergetter 方法来读取当前属性值。 Getter 方法实际上会计算并返回一个新的 Point 来表示正方形的中心,而不是返回一个现有的值。从上面代码可以看出,getter 方法正确地返回了一个中心点 (5, 5)

接下来我们将 center 属性设置为新的值 (15, 15) ,它会将方块向上和向右移动到下图中橙色方块所示的新位置。设置 center 属性会调用 centersetter 方法,它将会修改 origin 存储属性的 xy 值,并将方块移动到新的位置。

Setter 声明的速记符号

如果计算属性的 setter 方法没有为要设置的新值定义名称,则使用默认名称 newValue 。这是 Rect 结构体的替代版本,它使用了这种速记符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}

只读计算属性

只有 getter 方法但没有 setter 方法的计算属性称为 只读计算属性 。只读计算属性始终返回一个值,可以通过点语法访问,但不能给它赋值。

注意
你必须使用 var 关键字来声明计算属性(包括只读计算属性),这是因为它们的值是不固定。let 关键字仅用于常量属性,这种属性一旦被初始化以后,就不能再更改它们的值。

你还可以通过删除 get 关键字及其大括号来简化只读计算属性的声明:

1
2
3
4
5
6
7
8
9
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// 打印 "the volume of fourByFiveByTwo is 40.0"

这个例子定义了一个名为 Cuboid 的新结构体,它代表一个具有 widthheightdepth 属性的3D矩形框。这个结构体还有一个名为 volume 的只读计算属性,它计算并返回长方体的当前体积。 volume 可以被设置是没有意义的,因为对于特定的 volume 值,对应的 widthheightdepth 是不能唯一确定的。所以在这种情况下, Cuboid 提供只读计算属性以使外部用户能够获取当前计算体积是很有用的。

属性观察器

属性观察器会观察并对属性值的变化做出反应。每次设置属性值时都会调用属性观察器,即使新值与属性的当前值相同。

你可以将属性观察器添加到你定义的任何存储属性上,但延迟存储属性除外。你还可以通过在子类中重写属性来为任何继承的属性(无论是存储还是计算)添加属性观察器。你并不需要为非重写的计算属性定义属性观察器,因为你可以在计算属性的 setter 方法中观察并响应其值的更改。属性重写将在 重写 中有详细描述。

你可以选择在属性上定义一个或两个观察器:

  • 在存储值之前调用 willSet
  • 存储新值后立即调用 didSet

如果实现 willSet 观察器,它会将新属性值作为常量参数传递。你可以在 willSet 实现中指定此参数的名称。如果不在实现中指定参数名称,则使用默认参数名称 newValue

类似地,如果你实现一个 didSet 观察器,它会传递一个包含旧属性值的常量参数。你可以指定参数名称或使用默认参数名称 oldValue 。如果你在自己的 didSet 属性观察器里给自己赋值,那么你赋值的新值将会替代刚刚设置的值。

注意
在调用父类初始化方法之后,在子类中给父类属性赋值时,将会调用父类属性的 willSet 和 didSet 观察器。如果在调用父类初始化方法之前,在子类中给父类属性赋值,则不会调用父类的观察器。

下面是一个 willSetdidSet 的例子。示例中定义了一个名为 StepCounter 的类,它记录了一个人行走的总步数。该类可用于导入来自计步器或其它计步装置的数据,以追踪人们的日常运动情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("将要设置 totalSteps 为 \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("增加了 \(totalSteps - oldValue) 步")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// 将要设置 totalSteps 为 200
// 增加了 200 步
stepCounter.totalSteps = 360
// 将要设置 totalSteps 为 360
// 增加了 160 步
stepCounter.totalSteps = 896
// 将要设置 totalSteps 为 896
// 增加了 536 步

StepCounter 类声明了 Int 类型的 totalSteps 属性。这是一个带有 willSetdidSet 观察器的存储属性。

只要为属性赋了新值,就会调用 totalStepswillSetdidSet 观察器。即使新值与当前值相同,也是如此。

这个例子的 willSet 观察器使用了自定义参数名称 newTotalSteps 来表示即将到来的新值。在此示例中,它只是打印出即将设置的值。

在更新 totalSteps 的值之后调用 didSet 观察器。它将 totalSteps 的新值与旧值进行比较。如果步骤总数增加,则会打印一条消息,显示增加了多少步数。 didSet 观察器不会为旧值提供自定义参数名称,而是使用默认名称 oldValue

注意
如果将具有观察器的属性作为 in-out 参数传递给函数,则 willSetdidSet 观察器一定会被调用。这是因为 in-out 参数是 copy-in copy-out 内存模型:值一定会在函数结束后写回属性。

全局和局部变量

上面描述的用于计算和观察属性的功能也可用于 全局变量局部变量 。全局变量是指在任何函数、方法、闭包或类型上下文之外定义的变量。局部变量是指在函数、方法或闭包上下文中定义的变量。

你在前面章节中遇到的全局和局部变量都是 存储变量 。 存储变量(如存储属性)为特定类型的值提供存储,并允许设置和检索该值。

总之,你可以在全局或局部范围内定义 计算变量 和给存储变量设置观察器。计算变量只会计算它们的值,而不存储它们,编写方式与计算属性相同。

注意
全局常量和变量总是被延迟计算,与 延迟存储属性 类似。与延迟存储属性不同的是,全局常量和变量不需要使用 lazy 修饰符进行标记。

局部常量和变量永远不会被延迟计算。

类型属性

实例属性是属于特定类型的实例的属性。每次创建该类型的新实例时,它都有自己的一组属性值,与任何其他实例不同。

你还可以定义属于该类型本身的属性,而不是类型的实例属性。无论你创建的该类型的实例有多少,这些属性都只会有一个副本。这些属性称为 类型属性 。

类型属性用于定义一个对某个类型的 所有 实例都可见的值,例如所有实例都可以使用的常量属性(如 C 中的静态常量),或者所有实例都可以访问的全局变量属性(如 C 中的静态变量)。

存储类型属性可以是变量或常量。计算类型属性始终是变量属性,与声明计算实例属性的方式相同。

注意
与存储实例属性不同,你必须始终为存储类型属性提供默认值。这是因为类型本身没有初始化方法来给存储类型属性赋值。

存储类型属性在首次访问时被初始化。它们会被保证只初始化一次,即使同时由多个线程访问。请注意你并不需要用 lazy 修饰符标记它们。

类型属性的语法

CObjective-C 中,你使用 全局 静态变量来定义与类型关联的静态常量和变量。但是,在 Swift 中,类型属性是写在类型定义的花括号内,作为类型定义的一部分,并且每个类型属性都明确地显示它支持的类型。

你可以使用 static 关键字定义类型属性。对于类类型的计算类型属性,可以使用 class 关键字来允许子类覆盖超类的实现。下面的示例显示了存储和计算类型属性的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}

注意
上面的计算类型属性是只读计算类型属性,但你也可以使用与计算实例属性相同的语法定义读写计算类型属性。

检索和设置类型属性

类型属性可以使用点语法来进行检索和设置,就像实例属性一样。但是,类型属性是基于 类型 来进行检索和设置,而不是基于该类型的实例。 例如:

1
2
3
4
5
6
7
8
9
print(SomeStructure.storedTypeProperty)
// 打印 "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// 打印 "Another value."
print(SomeEnumeration.computedTypeProperty)
// 打印 "6"
print(SomeClass.computedTypeProperty)
// 打印 "27"

以下示例使用两个存储类型属性作为建模一个数字音频信道音频测量表的结构体的一部分。每个通道的整数音频电平在 010 之间。

下边的图例展示了这个音频频道如何组合建模一个立体声音频测量表。当通道的音频电平为 0 时,该通道的任何灯都不会亮起。当音频电平为 10 时,该通道的所有灯都会亮起。在该图中,左声道的当前电平为 9 ,右声道的当前电平为 7

上面描述的音频通道由 AudioChannel 结构体实例表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// 将新音频电平值限制在阈值之内
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// 将此存储为新的目前最大的电平值
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}

AudioChannel 结构体定义了两个存储类型属性以支持其功能。第一个, thresholdLevel ,定义音频电平值可以采用的最大阈值。对于所有 AudioChannel 实例,这是一个常量值 10 。(如下面描述的那样)如果音频信号电平值高于 10 ,我们仍然只能把它设置为 10

第二个类型属性是名为 maxInputLevelForAllChannels 的变量存储属性。这个变量保存 所有 AudioChannel 实例接收的最大输入值。它的初始值为 0

AudioChannel 结构体还定义了一个名为 currentLevel 的存储实例属性,它表示通道的当前音频电平值,范围为 010

currentLevel 属性有一个 didSet 属性观察器,可以在赋值时检查 currentLevel 的值。该观察器执行两项检查:

如果 currentLevel 的新值大于允许的 thresholdLevel ,则属性观察器将 currentLevel 限制为 thresholdLevel
如果 currentLevel 的新值(在阈值之内)高于 所有 AudioChannel 实例先前接收的电平值,则属性观察器将新的 currentLevel 值存储在 maxInputLevelForAllChannels 类型属性中。

注意
在第一个检查中,即使 didSet 观察器将 currentLevel 设置为不同的值,也不会导致再次调用观察器。

你可以使用 AudioChannel 结构体创建两个名为 leftChannelrightChannel 的音频通道,以表示立体声音响系统的音频等级:

1
2
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

如果将 左 通道的 currentLevel 设置为 7 ,则可以看到 maxInputLevelForAllChannels 类型属性更新为 7

1
2
3
4
5
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// 打印 "7"
print(AudioChannel.maxInputLevelForAllChannels)
// 打印 "7"

如果你试图将 右 通道的 currentLevel 设置为 11 ,你可以看到右通道的 currentLevel 属性的值是阈值 10 ,而 maxInputLevelForAllChannels 类型属性的值也更新为 10

1
2
3
4
5
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// 打印 "10"
print(AudioChannel.maxInputLevelForAllChannels)
// 打印 "10"