11.1. 存储属性

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

可以在定义存储属性的时候指定默认值,请参考 默认构造器 一节。也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值,请参考 自定义构造过程 的构造过程中常量属性的修改一节。

下面的例子定义了一个名为 FixedLengthRange 的结构体,该结构体用于描述整数的区间,且这个范围值在被创建后不能被修改。

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 在创建实例的时候被初始化,且之后无法修改它的值,因为它是一个常量存储属性。

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

如果创建了一个结构体实例并将其赋值给一个常量,则无法修改该实例的任何属性,即使被声明为可变属性也不行:

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 该区间表示整数 0,1,2,3
rangeOfFourItems.firstValue = 6
// 尽管 firstValue 是个可变属性,但这里还是会报错

因为 rangeOfFourItems 被声明成了常量(用 let 关键字),所以即使 firstValue 是一个可变属性,也无法再修改它了。

这种行为是由于结构体属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。

属于引用类型的类则不一样。把一个引用类型的实例赋给一个常量后,依然可以修改该实例的可变属性

延时加载存储属性

延时加载存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标示一个延时加载存储属性。

注意

必须将延时加载属性声明成变量(使用 var 关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延时加载。

属性的值依赖于一些外部因素且这些外部因素只有在构造过程结束之后才会知道的时候,延时加载属性就会很有用。或者当获得属性的值因为需要复杂或者大量的计算,而需要采用需要的时候再计算的方式,延时加载属性也会很有用。

下面的例子使用了延时加载存储属性来避免复杂类中不必要的初始化工作。例子中定义了 DataImporterDataManager 两个类,下面是部分代码:

class DataImporter {
    /*
    DataImporter 是一个负责将外部文件中的数据导入的类。
    这个类的初始化会消耗不少时间。
    */
    var fileName = "data.txt"
    // 这里会提供数据导入功能
}
class DataManager {
    lazy var importer = DataImporter()
    var data: [String] = []
    // 这里会提供数据管理功能
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter 实例的 importer 属性还没有被创建

DataManager 类包含一个名为 data 的存储属性,初始值是一个空的字符串数组。这里没有给出全部代码,只需知道 DataManager 类的目的是管理和提供对这个字符串数组的访问即可。

DataManager 的一个功能是从文件中导入数据。这个功能由 DataImporter 类提供,DataImporter 完成初始化需要消耗不少时间:因为它的实例在初始化时可能需要打开文件并读取文件中的内容到内存中。

DataManager 管理数据时也可能不从文件中导入数据。所以当 DataManager 的实例被创建时,没必要创建一个 DataImporter 的实例,更明智的做法是第一次用到 DataImporter 的时候才去创建它。

由于使用了 lazyDataImporter 的实例 importer 属性只有在第一次被访问的时候才被创建。比如访问它的属性 fileName 时:

print(manager.importer.fileName)
// DataImporter 实例的 importer 属性现在被创建了
// 输出“data.txt”

注意

如果一个被标记为 lazy 的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。

存储属性和实例变量

如果你有过 Objective-C 经验,应该知道 Objective-C 为类实例存储值和引用提供两种方法。除了属性之外,还可以使用实例变量作为一个备份存储将变量值赋值给属性。

Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的备份存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。属性的全部信息——包括命名、类型和内存管理特征——作为类型定义的一部分,都定义在一个地方。