Swift属性

目录

引入

属性将值与特定的类、 结构或枚举相关联。 存储属性将常量和变量值存储为实例的一部分, 而计算属性计算(而不是存储)一个值。

计算属性由类、 结构和枚举提供。 存储属性仅由类和结构提供。

存储属性和计算属性通常与特定类型的实例相关联。 不过, 属性也可以与类型本身相关联。 此类属性称为类型属性。

此外, 还可以定义属性观察者来监控属性值的变化, 并通过自定义操作对变化做出响应。 属性观察器可以添加到自己定义的存储属性中, 也可以添加到子类从其超类继承的属性中。

存储属性

最简单地说, 存储属性就是作为特定类或结构实例的一部分存储的常量或变量。

存储属性既可以是变量存储属性(由 var 关键字引入), 也可以是常量存储属性(由 let 关键字引入)。

可以为存储属性提供默认值, 作为其定义的一部分。 也可以在初始化过程中设置或修改存储属性的初始值。 即使是常量存储属性也是如此。

struct User {
    // 存储属性两个:name和age
    let name: String
    var age: Int
}

// 实例化时传入参数,将数据保存在实例的属性上
var user = User(name: "hzclog", age: 18)
print(user.name, user.age) // hzclog 18

// 修改实例属性
user.age = 30
print(user.name, user.age) // hzclog 30

常量结构实例的存储属性

如果创建的结构实例赋值给了常量, 就无法修改实例属性了, 即使属性声明为变量。

struct User {
    let name: String
    var age: Int
}

let user = User(name: "hzclog", age: 18)
user.age = 30 // 报错:Cannot assign to property: 'user' is a 'let' constant

因为结构是值类型, 所以结构实例被标记为常量以后不能修改。 引用类型不同, 类的实例指定为常量, 仍然可以修改属性。

懒存储属性

懒存储属性是一种在首次使用前不计算初始值的属性。 你可以在声明之前写入 lazy 修饰符来表示懒存储属性。

⚠️

懒存储属性必需始终声明为变量, 因为它的初始值在实例化完成后才能获取。 常量属性在初始化完成前必须始终有一个值, 因此不能声明为懒属性。

如果属性的初始值取决于外部因素, 而这些因素的值要到实例初始化完成后才能知道, 那么懒属性就非常有用。 当属性的初始值需要复杂或计算代价高昂的设置时,懒属性也很有用。

struct Importer {
    var fileName = "test.txt"
}

struct Manager {
    var data: [String] = []
    // 假设导入文件需要大量时间, 我们使用lazy修饰, 实际使用时再实例化importer
    lazy var importer = Importer()
}

var manager = Manager()
manager.data.append("hello.txt")
manager.data.append("world.txt")
print(manager.data) // ["hello.txt", "world.txt"]

// 到这个位置,importer还是没有实例化
// 直到下面的语句需要访问importer时,才做了实例化
print(manager.importer.fileName) // test.txt

计算属性

除了存储属性外, 类、 结构和枚举还可以定义计算属性, 这些属性实际上并不存储值。 相反, 它们提供了一个 getter 和一个可选的 setter, 用于间接检索和设置其他属性和值。

struct User {
    // 存储属性
    var birth = 1991
    
    // 计算属性 age,Int类型
    var age: Int {
        // 获取值
        get {
            return 2024 - birth
        }
        
        // 设置值
        set(newAge) {
            birth = 2024 - newAge
        }
    }
}

var user = User()
print(user.birth) // 1991
print(user.age) // 33

user.age = 18
print(user.birth) // 2006
print(user.age) // 18

速记Setter声明

如果计算属性的Setter没有设置新值的名称, 默认会使用 newValue

var age: Int {
    get {
        return 2024 - birth
    }
    
    set {
        birth = 2024 - newValue // 注意这里
    }
}

速记Getter声明

如果Getter的主体是一个表达式, Getter会隐式返回这个表达式结果。

var age: Int {
    get {
        2024 - birth // 可以省去return
    }
    
    set {
        birth = 2024 - newValue
    }
}

只读计算属性

只有getter, 没有setter的计算属性称为只读计算属性。 只读计算属性总是返回一个值, 但是不能设置为不同的值。

通过删除get关键字及大括号, 可以简化声明。

struct User {
    var birth = 1991
    
    // 只读计算属性age,省去get关键字
    var age: Int {
        2024 - birth
    }
}

var user = User()
print(user.age) // 33
⚠️

必须使用 var 声明计算属性(包括只读属性), 因为它们的值不是固定的。

属性观察器

属性观察期可以观察并响应属性值的变化。 每当设置属性值时, 即使新值与当前值完全相同, 也会调用属性观察器。

可以在一个属性上定义一个或两个观察器:

  • willset: 在存储值之前调用
  • didSet: 在存储值之后调用
struct StepCounter {
    var steps = 0 {
        // 默认newValue存有要设置的新值
        willSet {
            print("总计运动了\(newValue)步.")
        }
        
        // 默认oldValue存有之前的旧值
        didSet {
            if steps > oldValue {
                print("进步了\(steps - oldValue).")
            }
        }
    }
}

var counter = StepCounter()
counter.steps = 100
counter.steps = 1000

运行结果

总计运动了100步. 进步了100. 总计运动了1000步. 进步了900.

属性包装器

属性包装器在管理属性存储方式的代码和定义属性的代码之间增加一层隔离。 我们可以编写一次包装器管理代码, 然后应用到多个属性上, 不必重复编写。

// 定义屏幕宽度包装器
@propertyWrapper
struct ScreenWidth {
    private var width = 1920
    var wrappedValue: Int {
        get {
            return width
        }
        set {
            width = min(newValue, 1920)
        }
    }
}

// 定义屏幕高度包装器
@propertyWrapper
struct ScreenHeight {
    private var height = 1080
    var wrappedValue: Int {
        get {
            return height
        }
        set {
            height = min(newValue, 1080)
        }
    }
}

// 手机屏幕
struct PhoneScreen {
    // 使用@属性包装器名称 的方式应用到属性上
    @ScreenWidth var width: Int
    @ScreenHeight var height: Int
}

var screen = PhoneScreen()
print(screen.width, screen.height) // 初始分辨率 1920 1080

screen.width = 1280
screen.height = 720
print(screen.width, screen.height) // 修改分辨率 1280 720

screen.width = 2560
screen.height = 1440
print(screen.width, screen.height) // 因为属性包装器的限制,最大宽度高度是1920*1080,所以这里是1920 1080

设置属性包装的初始值

上面代码的属性包装初始值是被写死的1920和1080。 可以通过给属性包装器添加初始化器来支持自定义功能。

@propertyWrapper
struct SmallNumber {
    private var maxNumber: Int
    private var number: Int
    
    var wrappedValue: Int {
        get {
            return number
        }
        
        set {
            number = min(newValue, maxNumber)
        }
    }
    
    init(_ n: Int, _ maxN: Int) {
        self.maxNumber = maxN
        number = min(n, maxN)
    }
}

struct Rect {
    @SmallNumber(10, 100) var width: Int
    @SmallNumber(5, 50) var height: Int
}

var rect = Rect()
print(rect.width, rect.height) // 10 5

rect.width = 100
print(rect.width, rect.height) // 100 5

rect.height = 100
print(rect.width, rect.height) // 100 50

全局和局部变量

全局变量和局部变量也具有上述计算和观察属性的功能。

全局变量是在任何函数、 方法、 闭包之外定义的变量。 局部变量是在函数、 方法或闭包上下文中定义的变量。

存储变量就像存储属性一样, 为特定类型的值提供存储空间, 并允许设置和检索该值。 你也可以定义计算变量, 并在全局或局部范围内为存储变量定义观察者。 计算变量计算其值, 而不是存储其值, 它们的书写方式与计算属性相同。

你可以为局部存储变量应用属性包装器, 但不能为全局变量或计算变量应用属性包装器。

类型属性

实例属性是属于特定类型实例的属性。 每次创建该类型的新实例时, 它都有自己的一组属性值, 与其他任何实例分开。

你也可以定义属于类型本身的属性, 而不是属于该类型的任何一个实例的属性。 无论创建多少该类型的实例, 这些属性都只有一个副本。 这类属性称为类型属性。

与存储实例属性不同,存储类型属性必须始终具有默认值。这是因为类型本身没有初始化器,无法在初始化时为存储类型属性赋值。

类型属性语法

使用 static 关键字定义类型属性。

struct SomeThing {
    static var kind = "sth."
    static var computedKind: String {
        "something: \(kind)"
    }
}

print(SomeThing.kind) // sth.
print(SomeThing.computedKind) // something: sth.