您的位置:首页 - 教程 - Swift - 正文
Swift学习】Swift编程之旅---属性(十四)

  属性关联特定类、结构或枚举的值,存储属性将存储常量和变量作为实例的一部分,计算属性用于计算一个值,而不进行存储。计算属性可以用于类、结构体和枚举里,存储属性只能用于类和结构体。存储属性和计算属性通常用于特定类型的实例,但是,属性也可以直接用于类型本身,这种属性称为类型属性。另外,还可以定义属性监视器来观察属性值的变化,以此来触发一个自定义的操作。属性监视器可以添加到存储属性上,也可以添加到从父类继承的属性。

 

  Stored Properties存储属性

   存储属性可以是常量或变量,你可以给存储属性设置默认值也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值

struct FixedLengthRange { 
    var firstValue: Int 
    let length: Int 
} 
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3) 

rangeOfThreeItems.firstValue = 6 

FixedLengthRange的实例包含一个名为firstValue的变量存储属性和一个名为length的常量存储属性。在上面的例子中,length在创建实例的时候被赋值,因为它是一个常量存储属性,所以之后无法修改它的值。

 
  常量结构体的存储属性
  
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// this will report an error, even though firstValue is a variable property

 因为rangeOfFourItems声明成了常量,即使firstValue是一个变量属性,也无法再修改它了。这是由于结构体(struct)属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。属于引用类型的类(class)则不一样,把一个引用类型的实例赋给一个常量后,仍然可以修改实例的变量属性。

 

  延迟存储属性

延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用lazy关键字来申明一个延迟存储属性。  

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

延迟属性很有用,当属性的值依赖于在实例的构造过程结束前无法知道具体值的外部因素时,或者当属性的值需要复杂或大量计算时,可以只在需要的时候来计算它。
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的存储属性,初始值是一个空的字符串(String)数组。DataManager类的目的是管理和提供对这个字符串数组的访问。

DataManager的一个功能是从文件导入数据,该功能由DataImporter类提供,DataImporter需要消耗不少时间完成初始化:因为它的实例在初始化时可能要打开文件,还要读取文件内容到内存。
DataManager也可能不从文件中导入数据。所以当DataManager的实例被创建时,没必要立即创建一个DataImporter的实例,当用到DataImporter的时候才去创建它。

      

  计算属性

计算属性不直接存储值,而是提供一个 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))") 
// 输出 "square.origin is now at (10.0, 10.0)” 

以上定义了 3 个几何形状的结构体

Point封装了一个(x, y)的坐标

Size封装了一个width和height
Rect表示一个有原点和尺寸的矩形

Rect也提供了一个名为center的计算属性。一个矩形的中心点可以从原点和尺寸来算出,所以不需要将它以显式声明的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结构体代码:

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 的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。只读计算属性的声明可以去掉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)") 
// 输出 "the volume of fourByFiveByTwo is 40.0" 

 

 

  属性观察

  观察属性值的变化,当发生变化时实现自定义操作。可以为属性添加如下的一个或全部监视器:

  • willSet在属性值变化前调用
  • didSet在属性值变化后立即调用

willSet监视器会将新的属性值作为固定参数传入,在willSet的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称newValue表示。

似地,didSet监视器会将旧的属性值作为参数传入,可以为该参数命名或者使用默认参数名oldValue。

 注意:willSet和didSet监视器在属性初始化过程中不会被调用,他们只会当属性的值在初始化之外的地方被设置时被调用。

 

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("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,这个监视器只是简单的将新的值输出。
didSet监视器在totalSteps的值改变后被调用,它把新的值和旧的值进行对比,如果总的步数增加了,就输出一个消息表示增加了多少步。didSet没有提供自定义名称,所以默认值oldValue表示旧值的参数名。

注意:如果在didSet监视器里为属性赋值,这个值会替换监视器之前设置的值。

 

  类型属性

  实例属性是属于特定类型的实例的属性。每次创建该类型的新实例时,它都有它自己的一组属性值,与任何其他实例分离,也可以为类型本身定义属性,不管类型有多少个实例,这些属性都只有唯一一份。这种属性就是类型属性。

类型属性用于定义特定类型所有实例共享的数据,比如所有实例都能用的一个常量(就像 C 语言中的静态常量),或者所有实例都能访问的一个变量(就像 C 语言中的静态变量)。对于值类型(指结构体和枚举)可以定义存储型和计算型类型属性,对于类(class)则只能定义计算型类型属性。
 
值类型的存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算属性一样定义成变量属性。
 
注意:跟实例的存储属性不同,必须给存储型类型属性指定默认值,因为类型本身无法在初始化过程中使用构造器给类型属性赋值。

 

  类型属性语法

使用关键字static来定义值类型的类型属性,关键字class来为类(class)定义类型属性。下面的例子演示了存储型和计算型类型属性的语法

struct SomeStructure { 
    static var storedTypeProperty = "Some value." 
    static var computedTypeProperty: Int { 
    // 这里返回一个 Int 值 
    } 
} 
enum SomeEnumeration { 
    static var storedTypeProperty = "Some value." 
    static var computedTypeProperty: Int { 
    // 这里返回一个 Int 值 
    } 
} 
class SomeClass { 
    class var computedTypeProperty: Int { 
    // 这里返回一个 Int 值 
    } 
} 

 

  获取和设置类型属性的值

实例的属性一样,类型属性的访问也是通过点运算符来进行,但是,类型属性是通过类型本身来获取和设置,而不是通过实例。比如

println(SomeClass.computedTypeProperty) 
// 输出 "42" 
 
println(SomeStructure.storedTypeProperty) 
// 输出 "Some value." 
SomeStructure.storedTypeProperty = "Another value." 
println(SomeStructure.storedTypeProperty) 
// 输出 "Another value.” 

下面的例子定义了一个结构体,使用两个存储型类型属性来表示多个声道的声音电平值,每个声道有一个 0 到 10 之间的整数表示声音电平值。

 
后面的图表展示了如何联合使用两个声道来表示一个立体声的声音电平值。当声道的电平值是 0,没有一个灯会亮;当声道的电平值是 10,所有灯点亮。本图中,左声道的电平是 9,右声道的电平是 7。

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定义了 2 个存储型类型属性来实现上述功能。第一个是thresholdLevel,表示声音电平的最大上限阈值,它是一个取值为 10 的常量,对所有实例都可见,如果声音电平高于 10,则取最大上限值 10(见后面描述)。

 
第二个类型属性是变量存储型属性maxInputLevelForAllChannels,它用来表示所有AudioChannel实例的电平值的最大值,初始值是 0。
 
AudioChannel也定义了一个名为currentLevel的实例存储属性,表示当前声道现在的电平值,取值为 0 到 10。
 
属性currentLevel包含didSet属性监视器来检查每次新设置后的属性值,有如下两个检查:
 
如果currentLevel的新值大于允许的阈值thresholdLevel,属性监视器将currentLevel的值限定为阈值thresholdLevel。
如果修正后的currentLevel值大于任何之前任意AudioChannel实例中的值,属性监视器将新值保存在静态属性maxInputLevelForAllChannels中。
 
注意:在第一个检查过程中,didSet属性监视器将currentLevel设置成了不同的值,但这时不会再次调用属性监视器。
 
可以使用结构体AudioChannel来创建表示立体声系统的两个声道leftChannel和rightChannel:
var leftChannel = AudioChannel() 
var rightChannel = AudioChannel() 

 如果将左声道的电平设置成 7,类型属性maxInputLevelForAllChannels也会更新成 7:

leftChannel.currentLevel = 7 
println(leftChannel.currentLevel) 
// 输出 "7" 
println(AudioChannel.maxInputLevelForAllChannels) 

如果试图将右声道的电平设置成 11,则会将右声道的currentLevel修正到最大值 10,同时maxInputLevelForAllChannels的值也会更新到 10:

rightChannel.currentLevel = 11  
println(rightChannel.currentLevel)  
// 输出 "10"  
println(AudioChannel.maxInputLevelForAllChannels)  

评论: