属性
属性
属性关联数据到特定的类、结构体、或者枚举。存储属性(stored properties)存储了常量或者变量作为一个实例的一部分;计算属性(computed properties)计算(而不是存储)一个值。计算属性可以用在类、结构体和枚举中。存储属性只能用于类和结构体。
存储属性或计算属性和一个特定类型的实例结合在一起。However,properties can also be associated with the type itself. Such properties are known as type properties.
另外,你可以定义属性观察者来监视属性值的变化,可以做特定的响应行为。属性观察者可以根据你的设计添加在存储属性上,也可以加在子类继承自超类的属性上。
存储属性(Stored Properties)
最简单的形式,存储属性是作为一个特定类或者结构体的一部分被保存的常量或者变量。存储类型可以是变量存储类型(variable stored properties,采用var关键字)也可以是常量存储类型(constant stored properties,采用let关键字)。
你可以在定义中给出存储属性的默认值,就像 Default Property Values 一节描述的。你也可以在初始化时更改存储属性的默认值。对于常量存储属性也是适用的,就像 Modifying Constant Properties During Initialization一节描述的。
下面的例子定义了一个叫做FixedLegthRenge的结构体,它描述了一个整型序列,这个序列一旦在被创建后其长度就不能被修改了:
struct FixedLengthRange { var firstValue: Int let length: Int } var rangeOfThreeItems = FixedLengthRange(firstValue: 0,length: 3) // the range represents integer values 0,1,and 2 rangeOfThreeItems.firstValue = 6 // the range now represents integer values 6,7,and 8
FixedLengthRange实例有一个变量存储属性叫做firstValue还有一个常量存储属性叫做length。上面的例子中,length在构造一个新序列的时候被初始化,并且此后它不能被修改,因为它是一个常量。
常量结构体实例的存储属性
如果你创建一个结构体的实例并且将它赋值给一个常量,你不能修改引用的属性,即使那些属性被声明为变量:
let rangeOfFourItems = FixedLengthRange(firstValue: 0,length: 4) // this range represents integer values 0,2,and 3 rangeOfFourItems.firstValue = 6 // this will report an error,even though firstValue is a variable property
因为rangeOfFourItems被声明为一个常量(采用了let关键字),所以不能修改它的firstValue属性,尽管firstValue是一个变量。
这些更证明了结构体是值类型的。当一个值类型的引用被标记为常量,那么它的所有属性都不能修改。
同样的事儿不会在类上发生,类是引用类型的。如果你将一个引用类型的实例赋值给一个常量,你仍然可以修改那个实例的变量属性。
惰性存储属性
惰性存储属性是这样一类属性,它直到被第一次使用才会初始化。在一个属性声明之前采用lazy就标记了一个惰性属性。
NOTE
你可以总是定义一个惰性属性作为变量(使用var 关键字),因为直到初始化完成,它的值不会被使用。惰性属性在如下情形非常有用:一个属性的初始值需要复杂或者计算开销昂贵,这时会等到需要这个属性的时候才初始化。
下面的例子是一个惰性存储属性,避免了一个复杂类不必要的初始化。这个例子定义了两个类分别叫做DataImporter和DataManager,下面的代码只是它们各自内容的一部分:
class DataImporter { /* DataImporter is a class to import data from an external file. The class is assumed to take a non-trivial amount of time to initialize. */ var fileName = "data.txt" // the DataImporter class would provide data importing functionality here } class DataManager { lazy var importer = DataImporter() var data = [String]() // the DataManager class would provide data management functionality here } let manager = DataManager() manager.data.append("Some data") manager.data.append("Some more data") // the DataImporter instance for the importer property has not yet been created
DataManager类有一个存储属性叫做data,data被初始化为全新的空的一个String数组。尽管DataManager的其他功能没有列出来,但可以推测这个类的目的是管理data 和提供对data内数据的访问。
DataManager类的一个功能是从一个文件导入数据。这个功能由DataImporter类提供,这个类的初始化可能会花费很多时间。导致这样可能的原因是DataImporter类实例初始化时,需要打开一个文件并将其中的内容读入到内存中。
很可能DataManager类的实例可以管理它的数据而不从文件导入数据,所以没必要再DataManager类初始化的时候就创建一个DataImporter实例,反而应该是在需要DataImporter的时候再构造它。
因为被标记了lazy,DataImporter的实例作为importer属性只在importer属性被第一次访问时才会被创建,比如它的fileName属性被使用的时候:
println(manager.importer.fileName) // the DataImporter instance for the importer property has now been created // prints "data.txt"
存储属性和实例变量
如果你有OC的经验,你会知道OC提供了两种方式来在类实例中存储数据和引用。除了属性,你可以使用实例变量作为实际的后台存储。
Swift将这些概念统一到了一个属性的定义语句中。一个Swift属性没有对应的引用变量,而且后台存储是没法直接访问的。这就避免了在不同的上下文环境下数据如何被访问的混乱,而且简化了属性的定义在一个明确的语句中完成。属性的所有信息包括:名字、类型和内存管理方面的特性,这些作为类型定义的一部分在一处就被定义完成了。
计算属性(computed properties)
除了存储属性,类、结构体和枚举可以定义计算属性,计算属性并不存储值,而是提供了一getter和可选的setter来间接地获取或设置其他属性或者值。
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) println("square.origin is now at (\(square.origin.x),\(square.origin.y))") // prints "square.origin is now at (10.0,10.0)"
这个例子定义了三个结构体来表述集合图形:
1:Point封装了 (x,y)
2:Size封装了 width和height
3:Rect通过定义一个初始点(Point)和大小(Size)来定义一个矩形
Rect结构体定义了一个计算属性叫做center。一个Rect的当前中心点始终都是根据他的origin(原始点)和size(大小)推算而来,所以你不需要存储中心点数值(一个Point)。因此,Rect给计算属性center定义了一个定制的getter和setter,使得你可以处理矩形的中心center,就像实际了这么一个属性一样。
上例中,创建了一个新的Rect变量叫做square。square用一个点(0,0)和一个宽度(10)、高度(10)初始化。这个正方形在下图中用蓝色标识。
square变量的center属性接下来通过点号(square.center)访问,这会调用getter,进而得到当前属性的值。不是返回一个已经存在的值,getter是经过计算并且返回一个新的点标识正方形的中心点。就像上面你看到的一iyang,getter正确的返回了中心点(5,5)。
中心点接下来被设置为了一个新的值(15,15),就是像右上方向移动了这个正方形,下图中用桔子色标识。给center属性设置会调用setter,它会修改存储在origin属性中的x和y,并且将正方形移动到新位置。
简写的Setter
如果一个计算属性的setter没有定义一个新值的名字,默认的名字“newValue”将会被使用。这里有Rect结构体的非正式写法版本,展示了简写的好处:
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的计算属性,就是一个只读计算属性。一个只读计算属性总是会返回一个值,可以通过点号来访问但是不能被设值。
NOTE
使用var定义的变量如果想要只读,就必须设置为只读计算属性,因为变量的值是不固定的。let关键字只能定义常量,他们的值在给定后就不能修改了。
你可以简化只读计算属性的定义,就是去掉get关键字和它的花括号:
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) println("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)") // prints "the volume of fourByFiveByTwo is 40.0"
这个例子订购一了一个新的结构体叫做Cuboid(立方体),它表示一个3D的矩形盒子,长宽高是它的三个属性。这个结构体同样有一个只读计算属性叫做Volume,Volume计算并放回立方体的体积。让Volume是可以赋值的是不明智的,因为只有width、height和depth才能决定体积(Volume)。但是给Cuboid(立方体)提供一个只读的计算属性是有必要的,外部用户可以通过它获得当前计算的体积。
属性观察者
属性观察者观察和响应一个属性的值的改变。属性观察者在属性值每次被设置时被调用,即使新的值和原来的值是一样的。
你可以给你定义的任意存储属性添加属性观察者,但是惰性存储属性除外。通过子类覆盖,你同样可以给任意继承属性(不论是存储属性还是计算属性)添加属性观察者。属性覆盖参见 Overriding。
NOTE
你不必给非继承计算属性添加属性观察者,因为你可以观察和响应属性值的变化,通过计算属性的setter。
你可以选择给一个属性定义一个还是全部的观察者:
1:willSet 在值被存储前调用;
2:didSet 在值被存储后被调用。
willSet观察者接受新的属性值作为一个常量参数,你可以给这个参数指定一个名字。如果你选择不写参数名称和圆括号,将会采用默认的参数名newValue。
类似的,didSet观察者接受属性的旧值作为一个常量参数,愿意的化你可以给这个参数命名,没有命名时会有一个默认的参数名oldValue。
NOTE
willSet和didSet观察者在初始化时 给属性赋值时,不会被调用。
更多内容参见 Initializer Delegation for Value Types 和Initializer Delegation for Class Types.
这里有willSet和didSet的例子。下面的例子定义了一个新的类叫做SetCounter,用来记录一个人走路过程中所有的步数。这个类可以使用从计步器中的数据,用来计算一个人日常生活中的锻炼情况。
class StepCounter { var totalSteps: Int = 0 { willSet(newTotalSteps) { println("About to set totalSteps to \(newTotalSteps)") } didSet { if totalSteps > oldValue { println("Added \(totalSteps - oldValue) steps") } } } } let stepCounter = StepCounter() stepCounter.totalSteps = 200 // About to set totalSteps to 200 // Added 200 steps stepCounter.totalSteps = 360 // About to set totalSteps to 360 // Added 160 steps stepCounter.totalSteps = 896 // About to set totalSteps to 896 // Added 536 steps
StepCounter类定义了一个Int类型的属性totalSteps。这个存储属性有willSet和didSet观察者。
totalSteps的willSet和didSet观察者会在属性被赋值的时候被调用。即使新值和原来的值一样也会。
willSet给新来的值采用了一个自定义的参数名叫做newTotalSteps。例子中,新值会在即将赋值前辈打印出来。
当totalSteps被修改后,didSet会被调用。它里面会比较totalSteps的新旧两个值。如果总步数增张了,将会打印一条信息表明增加了多少步。didSet没有给出自定义的参数名,而是使用了默认的参数名oldValue。
NOTE
如果你在一个属性的didSet中修改了属性的值,你设置的值将会替代调用观察者之前刚刚设置的。
全局和本地变量
对于全局变量和局部变量,上面叙述的计算和观察技能也适用。全局变量是定义在函数、方法、闭包和任意类型之外的变量。本地变量是在函数、方法或者闭包中的变量。
前面你遇到的全局和本地变量都是存储变量。存储变量类似于存储属性,提供特定类型的值存储功能,值可以被设置和获取。
然而,你同样可以定义计算变量和观察者给变量,全局或本地的都可以。计算变量也是计算而不是存储,写法和计算属性一样。
NOTE
全局常量和变量经常是惰性计算的,类似Lazy Stored Properties一节。不同于惰性存储属性,全局常量和变量不需要用lazy标记。
本地常量和变量不可以是惰性计算的。
类型属性(Type Properties)
实例属性是一个属于特定类型的属性。每次你创建一个类型的实例,这个实例就有属于自己的属性,而且这些属性是与类型的其他实例独立开来的。
你也可以定义只属于类型自身而不属于任何一个该类型实例的属性。无论那个类型会被创建多少实例,这些属性永远只有一个。这一类的属性就被称作类型属性。
类型属性在一个特定的类型需要定义全体实例都通用的值的时候非常有用,比如所有实例都可以使用的常量属性(就像C语言中的静态常量),再比如一个全局的对于一个类型的所有实例都有效的变量(就像C语言中的静态变量)。
对于值类型(也就是结构体和枚举),你可以定义存储属性和计算属性 的类型属性。对于类,你只能定义计算属性的类型属性。
值类型的存储类型属性(类型的存储属性),可以是变量或者常量。计算类型属性通常被定义为变量,计算实例属性也是一样。
NOTE
不同于存储实例属性,你必须要给存储类型属性一个默认值。这是因为类型自身没有初始化函数可以给存储属性设置值。
PS:
strored instance property 存储的实例属性
stored type property 存储的类型属性
computed instance property 计算的实例属性
computed type property 计算的类型属性
类型属性的语法
在C和OC中,你可以定义静态常量或变量关联到一个类型上作为全局静态属性。Swift中,类型属性作为类定义的一部分被定义,写在类定义最外层的的花括号内,每个类型属性的作用与显然就是类型的作用域。
可以用关键字static定义值类型的类型属性;用class关键字定义类的类型属性。下面的例子展示了存储和计算的类型属性定义语法:
struct SomeStructure { static var storedTypeProperty = "Some value." static var computedTypeProperty: Int { // return an Int value here } } enum SomeEnumeration { static var storedTypeProperty = "Some value." static var computedTypeProperty: Int { // return an Int value here } } class SomeClass { class var computedTypeProperty: Int { // return an Int value here } }
NOTE
上面的计算类型属性是只读的,你也可以定义可读可写的计算属性参照计算实例属性的语法。
查找和设置类型属性
类型属性可以通过点号来操作,就像实例属性一样。然而,类型属性是作用在类型上的而不是一个类型的实例上。比如:
println(SomeClass.computedTypeProperty) // prints "42" println(SomeStructure.storedTypeProperty) // prints "Some value." SomeStructure.storedTypeProperty = "Another value." println(SomeStructure.storedTypeProperty) // prints "Another value."
下面例子中使用了一个结构体中的两个存储类型属性,它们的每个用来表示一个声音频道电平水平。每个声音频道的电平水平用整型0到10表示。
下图说明了两个声音频道组合成立一个立体声。当声音频道的生意是0,那个频道的灯全部灭掉。当频道的声音电平水平是10,那个频道的全部灯都会亮起来。图示中,左侧的电平水平是9,右侧的 是7:
上面所述的用一个叫做AudioChannel 的结构体的实例表示:
struct AudioChannel { static let thresholdLevel = 10 static var maxInputLevelForAllChannels = 0 var currentLevel: Int = 0 { didSet { if currentLevel > AudioChannel.thresholdLevel { // cap the new audio level to the threshold level currentLevel = AudioChannel.thresholdLevel } if currentLevel > AudioChannel.maxInputLevelForAllChannels { // store this as the new overall maximum input level AudioChannel.maxInputLevelForAllChannels = currentLevel } } } }
AudioChannel 结构体定义了两个存储类型属性来支持这个功能。首先,thresholdLevel(阀值),定义了可以设置的最大阀值。这是一个常量值(10)对于所有的实例都有效。如果音频信号大于10,将会采用这个阀值(像上面描述的一样)。
第二个类型属性是一个变量春初类型,叫做maxInputLevelForAllChannels。它记录所有AudioChanel类型的实例所接收到的最大输入值。它被赋值为0.
AudioChannel结构体同样定义了一个实例属性叫做currentLevel,在0-10这个区间表示当前的电平水平。
属性currentLevel有一个didSet观察者,来检查currentLevel的值设置。这个观察者做了两项检查:
1:如果新的currentLevel值大于thresholdLevel,用thresholdLevel取代currentLevel 。
2:如果新的currentLevel (经过1处理过的)大于任何一个AudioChannel实例收到的值 ,那么记录这个新值到类型属性maxInputLevelForAllChannels 中去。
NOTE
在两项检查中的第一项,didSet观察者给currentLevel设置了不同的值,但是这并不会再次调用观察者。
你可以采用AudioChannel结构体创建两个声音频道,分别叫做leftChannel和rightChannel,来表现立体声系统的电平书评:
var leftChannel = AudioChannel() var rightChannel = AudioChannel()
如果你设置左声道的currentLevel 为7,你会发现类型属性maxInputLevelForAllChannels 也被更新到了7:
leftChannel.currentLevel = 7 println(leftChannel.currentLevel) // prints "7" println(AudioChannel.maxInputLevelForAllChannels) // prints "7"
如果你尝试把右声道的currentLevel 设置为11,你会发现右侧的currentLevel 只能到10,类型属性maxInputLevelForAllChannels 被更新到了10:
rightChannel.currentLevel = 11 println(rightChannel.currentLevel) // prints "10" println(AudioChannel.maxInputLevelForAllChannels) // prints "10"