1、属性
属性将值跟特定的类、结构或枚举关联。存储属性(Stored properties)存储变量或常量的值,作为实例的一部分,而计算属性(computed properties) 则计算一个值。计算属性用于类、结构体和枚举。而存储属性只用于类和结构体。
存储属性和计算属性通常和某种类型的实例关联。然而,属性也可以和类型关联,这种属性称为类型属性(type properties)。
另外,还可以定义属性观察器(property observers )来监控属性值的变化,以此来触发一个自定义的操作。属性观察器可以添加到自己定义的存储属性上,也可以添加到从父类继承的属性上。
2、存储属性(Stored Properties)
简单来说,一个存储属性就是存储在特定类或结构体的实例里的一个常量或变量。存储属性可以是常量,也可以是变量。
存储属性可以在定义是设置默认值,也可以在实例初始化的时候设置和修改存储属性的值。
下例,在结构体中定义两个存储属性:
1 struct FixedLengthRange { 2 var firstValue: Int 3 let length: Int 4 } 5 var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3) 6 // the range represents integer values 0, 1, and 2 7 rangeOfThreeItems.firstValue = 6 8 // the range now represents integer values 6, 7, and 8
在上面的例子中,length
在创建实例的时候被初始化,因为它是一个常量存储属性,所以之后无法修改它的值。
(1)常量结构体实例的存储属性
如果你将一个结构体实例声明为常量,则不能修改该常量的属性,除非这些属性被声明称可变属性(variable properties)。
1 let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4) 2 // this range represents integer values 0, 1, 2, and 3 3 rangeOfFourItems.firstValue = 6 4 // this will report an error, even though firstValue is a variable property
由于rangeOfFourItems被声明为常量,所以它的属性firstValue不能被改变,除非firstValue是可变属性。
这种结果是由于结构体是值类型造成的。当值类型被标记成常量的时候,它的属性也是常量。
对于类,因为类是引用类型,即使它的实例被声明成常量,它的属性也可以被改变。
(2)惰性存储属性(Lazy Stored Properties)
惰性存储属性,是指只有当它第一次被使用的时候,才会对其进行初始化。声明一个惰性存储属性,在该属性前面加lazy关键字。
注意:惰性存储属性必须声明成变量,因为直到实例初始化完成,惰性存储属性的初始值才可能被取出。而常量属性总是在实例初始化完成之前就有一个初始值,因此,不能被声明为惰性。
1 class DataImporter { 2 /* 3 DataImporter is a class to import data from an external file. 4 The class is assumed to take a non-trivial amount of time to initialize. 5 */ 6 var fileName = "data.txt" 7 // the DataImporter class would provide data importing functionality here 8 } 9 10 class DataManager { 11 lazy var importer = DataImporter() 12 var data = [String]() 13 // the DataManager class would provide data management functionality here 14 } 15 16 let manager = DataManager() 17 manager.data.append("Some data") 18 manager.data.append("Some more data") 19 // the DataImporter instance for the importer property has not yet been created
DataManager
类包含一个存储属性data,初始值是一个空的字符串(String
)数组。虽然没有写出全部代码,DataManager
类的目的是管理和提供对这个字符串数组的访问。
DataManager
的一个功能是从文件导入数据。该功能由DataImporter
类提供,DataImporter
完成初始化需要消耗不少时间:因为它的实例在初始化时可能要打开文件,还要读取文件内容到内存。
DataManager
也可能不从文件中导入数据就完成了管理数据的功能。所以当DataManager
的实例被创建时,没必要创建一个DataImporter
的实例,更明智的是当第一次用到DataImporter
的时候才去创建它。
由于使用了lazy
,importer
属性只有在第一次被访问的时候才被创建。比如访问它的属性fileName
时:
1 print(manager.importer.fileName) 2 // the DataImporter instance for the importer property has now been created 3 // prints "data.txt"
注意:如果一个被标记为lazy
的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。
(3)存储属性和实例变量
Objective-C提供了两种方式来存储作为实例的一部分的值和引用,除了属性,你还可以用实例变量来作为存储在属性里的值的备份。
Swift把这些概念都用属性来实现。Swift中的属性没有对应的实例变量,它的备份存储值是不能直接访问的。这样,避免了对在不同场景下访问值的方式的困扰,也将属性的概念简化成一个语句。一个属性的全部信息——包括命名、类型和内存管理特征——都在类型定义中的相应位置完成。
3、计算属性
除存储属性外,类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。
1 struct Point { 2 var x = 0.0, y = 0.0 3 } 4 struct Size { 5 var width = 0.0, height = 0.0 6 } 7 struct Rect { 8 var origin = Point() 9 var size = Size() 10 var center: Point { 11 get { 12 let centerX = origin.x + (size.width / 2) 13 let centerY = origin.y + (size.height / 2) 14 return Point(x: centerX, y: centerY) 15 } 16 set(newCenter) { 17 origin.x = newCenter.x - (size.width / 2) 18 origin.y = newCenter.y - (size.height / 2) 19 } 20 } 21 } 22 var square = Rect(origin: Point(x: 0.0, y: 0.0), 23 size: Size(width: 10.0, height: 10.0)) 24 let initialSquareCenter = square.center 25 square.center = Point(x: 15.0, y: 15.0) 26 print("square.origin is now at (\(square.origin.x), \(square.origin.y))") 27 // prints "square.origin is now at (10.0, 10.0)"
这个例子定义了 3 个结构体来描述几何形状:
Point
封装了一个(x, y)
的坐标Size
封装了一个width
和一个height
Rect
表示一个有原点和尺寸的矩形
Rect
提供了一个计算属性center。一个矩形的中心点可以从原点(origin
)和尺寸(size
)算出,所以不需要将它以显式声明的Point
来保存。Rect
的计算属性center
提供了自定义的 getter 和 setter 来获取和设置矩形的中心点,就像它有一个存储属性一样。
square
的center
属性可以通过点运算符(square.center
)来访问,这会调用该属性的 getter 来获取它的值。跟直接返回已经存在的值不同,getter 实际上通过计算然后返回一个新的Point
来表示square
的中心点。如代码所示,它正确返回了中心点(5, 5)
。
center
属性之后被设置了一个新的值(15, 15)
。设置属性center
的值会调用它的 setter 来修改属性origin
的x
和y
的值,从而实现移动正方形到新的位置。
(1)setter的简写语法
计算属性的setter如果不定义新值的参数名,默认名称为newValue。
1 struct AlternativeRect { 2 var origin = Point() 3 var size = Size() 4 var center: Point { 5 get { 6 let centerX = origin.x + (size.width / 2) 7 let centerY = origin.y + (size.height / 2) 8 return Point(x: centerX, y: centerY) 9 } 10 set { 11 origin.x = newValue.x - (size.width / 2) 12 origin.y = newValue.y - (size.height / 2) 13 } 14 } 15 }
(2)只读计算属性
只有getter、没有setter的计算属性称为只读计算属性。
注意:计算属性(包括只读计算属性)只能声明为变量,因为它们的值不是固定不变的。
声明只读计算属性,可以省略get关键字和{}:
1 struct Cuboid { 2 var width = 0.0, height = 0.0, depth = 0.0 3 var volume: Double { 4 return width * height * depth 5 } 6 } 7 let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0) 8 print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)") 9 // prints "the volume of fourByFiveByTwo is 40.0"
4、属性观察器
属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,甚至新的值和现在的值相同的时候也不例外。
可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重载属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。
注意:不需要为非重载的计算属性添加属性观察器,因为可以通过它的 setter 直接监控和响应值的变化。
可以为属性添加如下的一个或全部观察器:
willSet
在新的值被设置之前调用didSet
在新的值被设置之后立即调用
willSet
观察器会将新的属性值作为常量参数传入,在willSet
的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称newValue
表示。
didSet
观察器会将旧的属性值作为参数传入,可以为该参数命名或者使用默认参数名oldValue
。
注意:父类的属性在子类的构造器中被赋值时,在父类构造器被调用之后,它在父类中的willSet
和didSet
观察器会被调用。
1 class StepCounter { 2 var totalSteps: Int = 0 { 3 willSet(newTotalSteps) { 4 print("About to set totalSteps to \(newTotalSteps)") 5 } 6 didSet { 7 if totalSteps > oldValue { 8 print("Added \(totalSteps - oldValue) steps") 9 } 10 } 11 } 12 } 13 let stepCounter = StepCounter() 14 stepCounter.totalSteps = 200 15 // About to set totalSteps to 200 16 // Added 200 steps 17 stepCounter.totalSteps = 360 18 // About to set totalSteps to 360 19 // Added 160 steps 20 stepCounter.totalSteps = 896 21 // About to set totalSteps to 896 22 // Added 536 steps
注意:如果在一个属性的didSet
观察器里为它赋值,这个值会替换该观察器之前设置的值。
如果你把一个拥有观察器的属性传递给函数的in-out参数,willSet和didSet会被调用。
5、全局变量和局部变量
计算属性和属性观察器所描述的模式也可以用于全局变量和局部变量。全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量。
前面章节提到的全局或局部变量都属于存储型变量,跟存储属性类似,它提供特定类型的存储空间,并允许读取和写入。
另外,在全局或局部范围都可以定义计算型变量,为存储型变量定义观察器。计算型变量跟计算属性一样,返回一个计算的值而不是存储值,声明格式也完全一样。
注意:全局的常量或变量都是延迟计算的,跟惰性存储属性相似,不同的地方在于,全局的常量或变量不需要标记lazy
特性。局部范围的常量或变量不会延迟计算。
6、类型属性(Type Properties)
实例属性属于一个特定类型实例,每次类型实例化后都拥有自己的一套属性值,实例之间的属性相互独立。
也可以为类型本身定义属性,不管类型有多少个实例,这些属性都只有唯一一份。这种属性就是类型属性。
类型属性用于定义特定类型所有实例共享的数据,比如所有实例都能用的一个常量(就像 C 语言中的静态常量),或者所有实例都能访问的一个变量(就像 C 语言中的静态变量)。
值类型的存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算属性一样只能定义成变量属性。
注意:
跟实例的存储属性不同,必须给存储型类型属性指定默认值,因为类型本身无法在初始化过程中使用构造器给类型属性赋值。
存储型类型属性是延迟初始化的(lazily initialized),它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行初始化一次,并且不需要对其使用 lazy
修饰符。
(1)类型属性语法
在 C 或 Objective-C 中,与某个类型关联的静态常量和静态变量,是作为全局静态变量定义的。但是在 Swift 中,类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内。
使用关键字static
来定义类型属性。在为类(class)定义计算型类型属性时,可以使用关键字class
来支持子类对父类的实现进行重写。
1 struct SomeStructure { 2 static var storedTypeProperty = "Some value." 3 static var computedTypeProperty: Int { 4 return 1 5 } 6 } 7 enum SomeEnumeration { 8 static var storedTypeProperty = "Some value." 9 static var computedTypeProperty: Int { 10 return 6 11 } 12 } 13 class SomeClass { 14 static var storedTypeProperty = "Some value." 15 static var computedTypeProperty: Int { 16 return 27 17 } 18 class var overrideableComputedTypeProperty: Int { 19 return 107 20 } 21 }
(2)获取和设置类型属性的值
跟实例的属性一样,类型属性的访问也是通过点运算符来进行。但是,类型属性是通过类型本身来获取和设置,而不是通过实例。
1 print(SomeStructure.storedTypeProperty) 2 // prints "Some value." 3 SomeStructure.storedTypeProperty = "Another value." 4 print(SomeStructure.storedTypeProperty) 5 // prints "Another value." 6 print(SomeEnumeration.computedTypeProperty) 7 // prints "6" 8 print(SomeClass.computedTypeProperty) 9 // prints "27"
下面是一个声道的结构体:
1 struct AudioChannel { 2 static let thresholdLevel = 10 3 static var maxInputLevelForAllChannels = 0 4 var currentLevel: Int = 0 { 5 didSet { 6 if currentLevel > AudioChannel.thresholdLevel { 7 // 将新电平值设置为阀值 8 currentLevel = AudioChannel.thresholdLevel 9 } 10 if currentLevel > AudioChannel.maxInputLevelForAllChannels { 11 // 存储当前电平值作为新的最大输入电平 12 AudioChannel.maxInputLevelForAllChannels = currentLevel 13 } 14 } 15 } 16 }
注意:
在didSet
属性观察器中,第一种情况将currentLevel
设置成了不同的值,但这时不会再次调用属性观察器。
可以使用结构体AudioChannel
来创建表示立体声系统的两个声道leftChannel
和rightChannel
:
1 var leftChannel = AudioChannel() 2 var rightChannel = AudioChannel()
如果将左声道的电平设置成 7,类型属性maxInputLevelForAllChannels
也会更新成 7:
1 leftChannel.currentLevel = 7 2 print(leftChannel.currentLevel) 3 // prints "7" 4 print(AudioChannel.maxInputLevelForAllChannels) 5 // prints "7"
如果试图将右声道的电平设置成 11,则会将右声道的currentLevel
修正到最大值 10,同时maxInputLevelForAllChannels
的值也会更新到 10:
1 rightChannel.currentLevel = 11 2 print(rightChannel.currentLevel) 3 // prints "10" 4 print(AudioChannel.maxInputLevelForAllChannels) 5 // prints "10"