指南:属性 (Properties)

  • 计算属性可以用于类、结构体和枚举,
  • 存储属性只能用于类和结构体。

枚举只能有计算属性

  • 属性观察器可以添加到自己定义的存储属性上,也可以添加到从父类继承的属性上。
  • 属性观察者是对存储属性而言的,类和结构体都可以用,但枚举不行。对于类来说,属性观察者可以观察基类成员。
  • 支持多用属性观察者,相比于KVO难用的API,这个方便多了
  • 属性也可以直接作用于类型本身,这种属性称为类型属性。

静态属性或者类属性

存储属性(Stored Properties)

  • 存储属性可以是变量存储属性(用关键字 var 定义),也可以是常量存储属性(用关键字 let 定义)。

默认都定义为let,实在有必要的才用var,好习惯一开始就要养成

  • 可以在定义存储属性的时候指定默认值,也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值。
  • ViewModel推荐给默认值,用普通变量。就算不显示,也先给一个“”
  • Model推荐不设默认值,用可选类型,模拟现实中收不到网络数据的情况
  • 如果创建了一个结构体的实例并将其赋值给一个常量,则无法修改该实例的任何属性,即使有属性被声明为变量也不行。
  • 这种行为是由于结构体(struct)属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。
  • 属于引用类型的类(class)则不一样。把一个引用类型的实例赋给一个常量后,仍然可以修改该实例的变量属性。
  • 延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。
  • 在属性声明前使用 lazy 来标示一个延迟存储属性。
    延迟属性很有用,当属性的值依赖于在实例的构造过程结束后才会知道影响值的外部因素时,或者当获得属性的初始值需要复杂或大量计算时,可以只在需要的时候计算它。
  • 必须将延迟存储属性声明成变量(使用 var 关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。
  • 如果一个被标记为 lazy 的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。也就是说lazy属性不是线程安全的,在多线程条件下要注意
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 属性还没有被创建

print(manager.importer.fileName)
// DataImporter 实例的 importer 属性现在被创建了
// 输出 "data.txt”
  • Swift 中的属性没有对应的实例变量,属性的后端存储也无法直接访问。

Object-C中属性其实是get、set函数,_属性名 是其背后的成员变量。一般情况,属性名用于外部访问,内部用下划线内部变量。在Swift中,就没有了到底用哪个的纠结了。

计算属性(Computed Properties)

  • 计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。
  • 如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。
  • 只有 getter 没有 setter 的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。
  • 必须使用 var 关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。
  • 只读计算属性的声明可以去掉 get 关键字和花括号

支持多用只读计算属性,多用这种方便写法

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)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// 输出 "square.origin is now at (10.0, 10.0)”

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)
        }
    }
}

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)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// 输出 "the volume of fourByFiveByTwo is 40.0"

属性观察器(Property Observers)

  • 属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。

  • 可以为除了延迟存储属性之外的其他存储属性添加属性观察器。

lazy 修饰的属性不能添加属性观察器

  • 可以通过重写属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。

  • 不必为非重写的计算属性添加属性观察器,因为可以通过它的getter, setter 直接监控和响应值的变化。

  • willSet 在新的值被设置之前调用

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

  • didSet 在新的值被设置之后立即调用

didSet 观察器会将旧的属性值作为参数传入,可以为该参数命名或者使用默认参数名 oldValue。如果在 didSet 方法中再次对该属性赋值,那么新值会覆盖旧的值。

  • 如果将属性通过 in-out 方式传入函数,willSet 和 didSet 也会调用。这是因为 in-out 参数采用了拷入拷出模式:即在函数内部使用的是参数的 copy,函数结束后,又对参数重新赋值。

  • 父类的属性在子类的构造器中被赋值时,它在父类中的 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

全局变量和局部变量(Global and Local Variables)

  • 计算属性和属性观察器所描述的功能也可以用于全局变量和局部变量。
  • 在全局或局部范围都可以定义计算型变量和为存储型变量定义观察器。计算型变量跟计算属性一样,返回一个计算结果而不是存储值,声明格式也完全一样。
  • 全局的常量或变量都是延迟计算的,跟延迟存储属性相似,不同的地方在于,全局的常量或变量不需要标记lazy修饰符。
  • 局部范围的常量或变量从不延迟计算。

类型属性(Type Properties)

  • 类型属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量(就像 C 语言中的静态常量),或者所有实例都能访问的一个变量(就像 C 语言中的静态变量)。
  • 存储型类型属性可以是变量或常量,计算型类型属性只能定义成变量属性。
  • 跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。
  • 存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。
  • 类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内。
  • 使用关键字 static 来定义类型属性。
  • 在为类定义计算型类型属性时,可以改用关键字 class 来支持子类对父类的实现进行重写。
  • 跟实例属性一样,类型属性也是通过点运算符来访问。但是,类型属性是通过类型本身来访问,而不是通过实例。
struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}
enum SomeEnumeration {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 6
    }
}
class SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107    // 类,计算属性,重写
    }
}

print(SomeStructure.storedTypeProperty)
// 输出 "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// 输出 "Another value.”
print(SomeEnumeration.computedTypeProperty)
// 输出 "6"
print(SomeClass.computedTypeProperty)
// 输出 "27"
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
            }
        }
    }
}
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

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

rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// 输出 "10"
print(AudioChannel.maxInputLevelForAllChannels)
// 输出 "10"
  • 类变量用static修饰,在一个struct内,用上了let或者var,这种方式可以替代define
  • 孤零零的全局变量尽量不要用,放在某个struct内进行归类。实在想用,也可以包装成一个单例再用。虽然不需要写external,用起来方便,但全局变量的副作用一点也没有被限制。
  • 计算属性是getter,setter;属性观察器是willSet和didSet;newValue,oldValue是隐含变量,方便使用;属性观察器需要给默认值,初始化(如果是存储属性);计算属性本质上像函数,不需要初始化,不需要默认值。这两者在代码结构上有点类似,需要区分清楚
  • lazy 标记的属性要按普通的属性一样初始化。只不过其真正执行初始化代码在调用的时候。
  • 属性观察器可以替代部分KVO的功能,但是用起来比KVO方便多了,鼓励多用。
  • 类的类属性,也统一用static吧,跟struct一样,好记又方便,基本就可以了。用class,计算属性,类才行,还要重写基类属性。这么复杂,有必要用吗?

你可能感兴趣的:(指南:属性 (Properties))