属性
属性将值与特定的类、结构体或枚举关联。存储属性会将常量和变量存储为实例的一部分,而计算属性则是直接计算(而不是存储)值。计算属性可以用于类、结构体和枚举,而存储属性只能用于类和结构体。
存储属性和计算属性通常与特定类型的实例关联。但是,属性也可以直接与类型本身关联,这种属性称为类型属性。
另外,还可以定义属性观察器来监控属性值的变化,以此来触发自定义的操作。属性观察器可以添加到类本身定义的存储属性上,也可以添加到从父类继承的属性上。
你也可以利用属性包装器来复用多个属性的 getter 和 setter 中的代码。
存储属性
简单来说,一个存储属性就是存储在特定类或结构体实例里的一个常量或变量。存储属性可以是 变量存储属性 (用关键字 var
定义),也可以是 常量存储属性 (用关键字 let
定义)。
可以在定义存储属性的时候指定默认值,请参考 默认构造器 一节。也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值,请参考 构造过程中常量属性的修改 一节。
下面的例子定义了一个名为 FixedLengthRange
的结构体,该结构体用于描述整数的区间,且这个范围值在被创建后不能被修改。
structFixedLengthRange { 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
关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延时加载。
当属性的值依赖于一些外部因素且这些外部因素只有在构造过程结束之后才会知道的时候,延时加载属性就会很有用。或者当获得属性的值因为需要复杂或者大量的计算,而需要采用需要的时候再计算的方式,延时加载属性也会很有用。
下面的例子使用了延时加载存储属性来避免复杂类中不必要的初始化工作。例子中定义了 DataImporter
和 DataManager
两个类,下面是部分代码:
classDataImporter { /* DataImporter 是一个负责将外部文件中的数据导入的类。 这个类的初始化会消耗不少时间。 */ var fileName ="data.txt" // 这里会提供数据导入功能 } classDataManager { lazyvar 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
的时候才去创建它。
由于使用了 lazy
,DataImporter
的实例 importer
属性只有在第一次被访问的时候才被创建。比如访问它的属性 fileName
时:
print(manager.importer.fileName) // DataImporter 实例的 importer 属性现在被创建了 // 输出“data.txt”
注意
如果一个被标记为
lazy
的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。
存储属性和实例变量
如果你有过 Objective-C 经验,应该知道 Objective-C 为类实例存储值和引用提供两种方法。除了属性之外,还可以使用实例变量作为一个备份存储将变量值赋值给属性。
Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的备份存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。属性的全部信息——包括命名、类型和内存管理特征——作为类型定义的一部分,都定义在一个地方。
计算属性
除存储属性外,类、结构体和枚举还可以定义 计算属性 。计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。
structPoint { var x =0.0, y =0.0 } structSize { var width =0.0, height =0.0 } structRect { var origin =Point() var size =Size() var center: Point { get { let centerX = origin.x + (size.width /2) let centerY = origin.y + (size.height /2) returnPoint(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 // initialSquareCenter 位于(5.0, 5.0) 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)”
这个例子定义了 3 个结构体来描述几何形状:
-
Point
封装了一个(x, y)
的坐标 -
Size
封装了一个width
和一个height
-
Rect
表示一个有原点和尺寸的矩形
Rect
也提供了一个名为 center
的计算属性。一个 Rect
的中心点可以从 origin
(原点)和 size
(大小)算出,所以不需要将中心点以 Point
类型的值来保存。Rect
的计算属性 center
提供了自定义的 getter 和 setter 来获取和设置矩形的中心点,就像它有一个存储属性一样。
上述例子中创建了一个名为 square
的 Rect
实例,初始值原点是 (0, 0)
,宽度高度都是 10
。如下图中蓝色正方形所示。
square
的 center
属性可以通过点运算符(square.center
)来访问,这会调用该属性的 getter 来获取它的值。跟直接返回已经存在的值不同,getter 实际上是通过计算然后返回一个新的 Point
来表示 square
的中心点。如代码所示,它正确返回了中心点 (5, 5)
。
center
属性之后被设置了一个新的值 (15, 15)
,表示向右上方移动正方形到如下图橙色正方形所示的位置。设置属性 center
的值会调用它的 setter 来修改属性 origin
的 x
和 y
的值,从而实现移动正方形到新的位置。
简化 Setter 声明
如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue
。下面是使用了简化 setter 声明的 Rect
结构体代码:
structAlternativeRect { var origin =Point() var size =Size() var center: Point { get { let centerX = origin.x + (size.width /2) let centerY = origin.y + (size.height /2) returnPoint(x: centerX, y: centerY) } set { origin.x = newValue.x - (size.width /2) origin.y = newValue.y - (size.height /2) } } }
简化 Getter 声明
如果整个 getter 是单一表达式,getter 会隐式地返回这个表达式结果。下面是另一个版本的 Rect
结构体,用到了简化的 getter 和 setter 声明:
structCompactRect { var origin =Point() var size =Size() var center: Point { get { Point(x: origin.x + (size.width /2), y: origin.y + (size.height /2)) } set { origin.x = newValue.x - (size.width /2) origin.y = newValue.y - (size.height /2) } } }
在 getter 中忽略 return
与在函数中忽略 return
的规则相同,请参考 隐式返回的函数。
只读计算属性
只有 getter 没有 setter 的计算属性叫 只读计算属性 。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。
注意
必须使用
var
关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let
关键字只用来声明常量属性,表示初始化后再也无法修改的值。
只读计算属性的声明可以去掉 get
关键字和花括号:
structCuboid { 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
的结构体,表示三维空间的立方体,包含 width
、height
和 depth
属性。结构体还有一个名为 volume
的只读计算属性用来返回立方体的体积。为 volume
提供 setter 毫无意义,因为无法确定如何修改 width
、height
和 depth
三者的值来匹配新的 volume
。然而,Cuboid
提供一个只读计算属性来让外部用户直接获取体积是很有用的。