MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift属性观察者与键值编码

2023-09-011.1k 阅读

Swift 属性观察者

1. 什么是属性观察者

在 Swift 中,属性观察者(Property Observers)是一种机制,允许我们在属性值发生变化时执行特定的代码。无论是存储属性还是计算属性(对于计算属性,只有在设置新值时才会触发属性观察者相关逻辑,因为计算属性本身不存储值),都可以添加属性观察者。属性观察者可以对属性值的变化进行监控,然后做出相应的响应,这在很多场景下非常有用,比如数据验证、日志记录以及更新相关联的其他属性等。

2. 属性观察者的类型

Swift 提供了两种类型的属性观察者:willSetdidSet

  • willSet:在属性值即将被设置新值之前调用。它会传递一个临时常量(默认为 newValue,也可以自定义名称),这个常量包含了即将被设置的新值。
  • didSet:在属性值已经被设置新值之后调用。它会传递一个临时常量(默认为 oldValue,也可以自定义名称),这个常量包含了属性原来的值。

3. 存储属性的属性观察者示例

class Temperature {
    var currentTemperature: Double {
        willSet(newTemperature) {
            print("温度即将从 \(currentTemperature) 变为 \(newTemperature)")
        }
        didSet(oldTemperature) {
            if currentTemperature != oldTemperature {
                print("温度已从 \(oldTemperature) 变为 \(currentTemperature)")
            }
        }
    }

    init(temperature: Double) {
        currentTemperature = temperature
    }
}

let roomTemperature = Temperature(temperature: 25.0)
roomTemperature.currentTemperature = 26.0

在上述代码中,Temperature 类有一个存储属性 currentTemperaturewillSet 块在新值被设置前打印提示信息,didSet 块在新值设置后检查值是否真的发生变化,并打印相应信息。

4. 计算属性的属性观察者

虽然计算属性本身不存储值,但可以在其 setter 中使用属性观察者的概念。

class Rectangle {
    var width: Double
    var height: Double

    init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }

    var area: Double {
        get {
            return width * height
        }
        set(newArea) {
            let ratio = sqrt(newArea / (width * height))
            width *= ratio
            height *= ratio
        }
    }
}

let rect = Rectangle(width: 10.0, height: 5.0)
print("初始面积: \(rect.area)")
rect.area = 100.0
print("新的面积: \(rect.area)")
print("新的宽度: \(rect.width), 新的高度: \(rect.height)")

这里的 area 是一个计算属性,在其 setter 中,通过改变 widthheight 来达到设置新的 area 的目的。虽然没有直接使用 willSetdidSet,但 setter 整体起到了类似属性观察者在值变化时执行特定逻辑的作用。

5. 继承与属性观察者

当子类继承父类并覆盖具有属性观察者的属性时,子类可以重新定义 willSetdidSet 行为。

class Animal {
    var name: String {
        willSet {
            print("\(self.name) 即将改名为 \(newValue)")
        }
        didSet {
            if name != oldValue {
                print("\(oldValue) 已改名为 \(name)")
            }
        }
    }

    init(name: String) {
        self.name = name
    }
}

class Dog: Animal {
    override var name: String {
        willSet {
            print("小狗 \(self.name) 即将改名为 \(newValue)")
        }
        didSet {
            if name != oldValue {
                print("小狗 \(oldValue) 已改名为 \(name)")
            }
        }
    }
}

let dog = Dog(name: "Buddy")
dog.name = "Max"

在这个例子中,Dog 类继承自 Animal 类,并重新定义了 name 属性的 willSetdidSet 行为,使得在小狗改名时打印更具体的信息。

键值编码(Key - Value Coding,KVC)

1. 什么是键值编码

键值编码(KVC)是一种通过键(通常是字符串)来间接访问对象属性的机制。在 Objective - C 中广泛使用,Swift 也支持 KVC 以保持与 Objective - C 的兼容性以及提供一种灵活的属性访问方式。KVC 允许开发者以一种统一的方式访问和修改对象的属性,而不需要直接调用对象的访问器方法(getter 和 setter)。这在很多动态编程场景,比如数据绑定、集合操作以及序列化等方面非常有用。

2. KVC 基础原理

KVC 基于对象的属性名(键)来查找和操作对应的值。当使用 KVC 访问一个属性时,系统会按照一定的搜索模式来查找对应的访问器方法或实例变量。如果对象响应以属性名命名的访问器方法(如 get<PropertyName>is<PropertyName> 对于布尔属性,以及 set<PropertyName>: 用于设置值),则会调用这些方法。如果没有找到访问器方法,系统会尝试直接访问实例变量(以下划线开头的变量名优先)。

3. 使用 KVC 访问属性

class Person {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let person = Person(name: "Alice", age: 30)
if let name = person.value(forKey: "name") as? String {
    print("名字: \(name)")
}
if let age = person.value(forKey: "age") as? Int {
    print("年龄: \(age)")
}

在上述代码中,通过 value(forKey:) 方法,使用 KVC 方式访问 Person 对象的 nameage 属性。value(forKey:) 方法返回一个可选的 Any 类型,需要进行类型转换。

4. 使用 KVC 设置属性

class Student {
    var grade: Int

    init(grade: Int) {
        self.grade = grade
    }
}

let student = Student(grade: 85)
student.setValue(90, forKey: "grade")
print("新的成绩: \(student.grade)")

这里使用 setValue(_:forKey:) 方法,通过 KVC 设置 Student 对象的 grade 属性值。

5. KVC 与集合操作

KVC 对集合(如 NSArrayNSDictionary)提供了强大的操作支持。可以通过 KVC 在集合上执行聚合操作,如求平均值、最大值、最小值等。

class Book {
    var title: String
    var price: Double

    init(title: String, price: Double) {
        self.title = title
        self.price = price
    }
}

let book1 = Book(title: "Swift Programming", price: 49.99)
let book2 = Book(title: "iOS Development", price: 59.99)
let book3 = Book(title: "Data Structures in Swift", price: 39.99)

let books = [book1, book2, book3]
if let averagePrice = (books as NSArray).value(forKeyPath: "average.price") as? Double {
    print("平均价格: \(averagePrice)")
}
if let maxPrice = (books as NSArray).value(forKeyPath: "max.price") as? Double {
    print("最高价格: \(maxPrice)")
}

在这个例子中,通过 value(forKeyPath:) 方法对 books 数组中的 Book 对象执行聚合操作。average.pricemax.price 是键路径,表示对数组中每个对象的 price 属性进行相应的聚合计算。

6. 键路径(Key Paths)

键路径是 KVC 的一个重要概念,它允许通过一系列键来访问对象图中的深层属性。例如,如果有一个包含多个嵌套对象的结构,可以使用键路径一次性访问到深层对象的属性。

class Address {
    var city: String

    init(city: String) {
        self.city = city
    }
}

class Employee {
    var name: String
    var address: Address

    init(name: String, address: Address) {
        self.name = name
        self.address = address
    }
}

let address = Address(city: "New York")
let employee = Employee(name: "Bob", address: address)
if let city = employee.value(forKeyPath: "address.city") as? String {
    print("员工地址的城市: \(city)")
}

这里通过 address.city 这样的键路径,直接访问到 Employee 对象中嵌套的 Address 对象的 city 属性。

7. KVC 的局限性与注意事项

  • 性能问题:KVC 是一种动态机制,相比直接访问属性,它涉及到更多的查找和反射操作,性能上会有一定的开销。特别是在性能敏感的代码中,频繁使用 KVC 可能会影响程序的运行效率。
  • 类型安全:KVC 是基于字符串键的,在编译时无法检测到键的拼写错误。如果使用了错误的键,运行时才会出现问题,这增加了调试的难度。
  • 兼容性:虽然 Swift 支持 KVC,但它主要是为了与 Objective - C 兼容。在纯 Swift 项目中,尽量优先使用 Swift 原生的属性访问和类型安全机制,只有在必要时才使用 KVC。

属性观察者与键值编码的结合应用

1. 数据验证与一致性维护

在使用 KVC 设置属性值时,可以结合属性观察者进行数据验证和一致性维护。

class Product {
    var price: Double {
        willSet {
            guard newValue >= 0 else {
                print("价格不能为负数")
                return
            }
        }
        didSet {
            if price != oldValue {
                print("价格已更新为 \(price)")
            }
        }
    }

    init(price: Double) {
        self.price = price
    }
}

let product = Product(price: 10.0)
product.setValue(-5.0, forKey: "price")
product.setValue(15.0, forKey: "price")

在这个例子中,Product 类的 price 属性有属性观察者。当使用 KVC 设置 price 值时,willSet 块会验证新值是否为负数,didSet 块会在值确实改变时打印更新信息。

2. 动态更新相关属性

假设一个 Circle 类,有 radiusarea 属性,area 是基于 radius 计算的。当通过 KVC 改变 radius 时,可以利用属性观察者更新 area

class Circle {
    var radius: Double {
        didSet {
            area = Double.pi * radius * radius
        }
    }
    var area: Double

    init(radius: Double) {
        self.radius = radius
        self.area = Double.pi * radius * radius
    }
}

let circle = Circle(radius: 5.0)
print("初始半径: \(circle.radius), 初始面积: \(circle.area)")
circle.setValue(7.0, forKey: "radius")
print("新的半径: \(circle.radius), 新的面积: \(circle.area)")

这里 radius 属性的 didSet 观察者在 radius 值改变时(无论是通过常规方式还是 KVC),自动更新 area 属性。

3. 集合数据的一致性管理

在集合中使用 KVC 进行批量操作时,属性观察者可以确保集合内对象的属性一致性。

class Fruit {
    var name: String
    var quantity: Int {
        didSet {
            if quantity < 0 {
                quantity = 0
                print("数量不能为负数,已修正为 0")
            }
        }
    }

    init(name: String, quantity: Int) {
        self.name = name
        self.quantity = quantity
    }
}

let apple = Fruit(name: "Apple", quantity: 5)
let banana = Fruit(name: "Banana", quantity: 3)
let fruits = [apple, banana]

(fruits as NSArray).setValue(-2, forKeyPath: "quantity")
for fruit in fruits {
    print("\(fruit.name): \(fruit.quantity)")
}

在这个例子中,Fruit 类的 quantity 属性有 didSet 观察者来确保数量不会为负数。通过 KVC 对 fruits 数组中的所有 Fruit 对象的 quantity 属性进行批量设置时,didSet 观察者会修正负数的情况。

深入理解与高级应用

1. KVC 与 Key - Value Observing(KVO)的关系

KVO(键值观察)是基于 KVC 的一种机制,它允许对象监听其他对象属性值的变化。KVO 依赖于 KVC 的键路径概念来确定要观察的属性。通过注册为观察者,一个对象可以在被观察对象的指定属性值发生变化时收到通知。

class Subject {
    var value: Int = 0 {
        didSet {
            willChangeValue(forKey: "value")
            // 执行其他逻辑
            didChangeValue(forKey: "value")
        }
    }
}

class Observer {
    var subject: Subject

    init(subject: Subject) {
        self.subject = subject
        subject.addObserver(self, forKeyPath: "value", options: [.new, .old], context: nil)
    }

    deinit {
        subject.removeObserver(self, forKeyPath: "value")
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "value", let new = change?[.newKey] as? Int, let old = change?[.oldKey] as? Int {
            print("值从 \(old) 变为 \(new)")
        }
    }
}

let subject = Subject()
let observer = Observer(subject: subject)
subject.value = 10

在这个示例中,Observer 类通过 KVO 监听 Subject 类的 value 属性变化。Subject 类在 value 属性变化时,通过 willChangeValue(forKey:)didChangeValue(forKey:) 通知 KVO 系统。

2. 自定义 KVC 行为

在某些特殊情况下,可能需要自定义 KVC 的查找和设置行为。可以通过重写 value(forKey:)setValue(_:forKey:) 方法来实现。

class CustomClass {
    private var customDictionary: [String: Any] = [:]

    override func value(forKey key: String) -> Any? {
        return customDictionary[key]
    }

    override func setValue(_ value: Any?, forKey key: String) {
        customDictionary[key] = value
    }
}

let customObject = CustomClass()
customObject.setValue("Custom Value", forKey: "customProperty")
if let customValue = customObject.value(forKey: "customProperty") as? String {
    print("自定义属性值: \(customValue)")
}

在这个 CustomClass 中,重写了 value(forKey:)setValue(_:forKey:) 方法,使得 KVC 操作基于一个自定义的字典,而不是传统的属性访问方式。

3. 性能优化与权衡

在使用属性观察者和 KVC 时,性能是一个需要考虑的重要因素。属性观察者本身的开销相对较小,但如果在观察者中执行复杂的操作,可能会影响性能。对于 KVC,由于其动态查找和反射机制,性能开销较大。在性能敏感的代码区域,应尽量避免频繁使用 KVC,或者通过缓存键路径等方式来优化性能。

class PerformanceClass {
    var property: Int

    init(property: Int) {
        self.property = property
    }

    // 缓存键路径
    static let propertyKeyPath = \PerformanceClass.property

    func performAction() {
        // 使用缓存的键路径进行操作
        let value = self[keyPath: PerformanceClass.propertyKeyPath]
        print("属性值: \(value)")
    }
}

let performanceObject = PerformanceClass(property: 42)
performanceObject.performAction()

这里通过缓存键路径(在 Swift 4.2 及以后版本支持),减少了每次使用 KVC 时的查找开销,从而提高性能。

与其他编程范式的结合

1. 函数式编程与属性观察者

在函数式编程中,强调不可变数据和纯函数。虽然属性观察者主要用于可变状态的管理,但可以与函数式编程的思想相结合。例如,可以在属性观察者中创建不可变的数据副本,并基于这些副本执行函数式操作。

class ImmutableData {
    private var _data: [Int] = []
    var data: [Int] {
        get {
            return _data
        }
        set {
            willSet {
                let newImmutableData = newValue.map { $0 }
                print("即将设置新的不可变数据: \(newImmutableData)")
            }
            _data = newValue
        }
    }

    init(data: [Int]) {
        self.data = data
    }
}

let immutableObject = ImmutableData(data: [1, 2, 3])
immutableObject.data = [4, 5, 6]

在这个例子中,ImmutableData 类的 data 属性在设置新值前,通过 willSet 观察者创建新数据的不可变副本,并打印相关信息。

2. 面向对象编程与 KVC

KVC 为面向对象编程提供了一种灵活的属性访问方式,特别是在继承和多态的场景下。不同子类可以通过 KVC 以统一的方式访问和设置属性,而不需要关心具体的子类类型。

class Shape {
    var color: String = "black"
}

class Rectangle: Shape {
    var width: Double
    var height: Double

    init(width: Double, height: Double) {
        self.width = width
        self.height = height
        super.init()
    }
}

class Circle: Shape {
    var radius: Double

    init(radius: Double) {
        self.radius = radius
        super.init()
    }
}

let rectangle = Rectangle(width: 10.0, height: 5.0)
let circle = Circle(radius: 3.0)

let shapes = [rectangle, circle]
for shape in shapes {
    if let color = shape.value(forKey: "color") as? String {
        print("形状的颜色: \(color)")
    }
}

这里 RectangleCircle 类继承自 Shape 类,通过 KVC 可以统一访问它们从 Shape 类继承的 color 属性。

3. 响应式编程与属性观察者

响应式编程强调数据流和变化传播。属性观察者天然适合响应式编程的场景,因为它们可以在属性值变化时触发相应的操作,类似于响应式编程中的事件驱动机制。

class ReactiveValue {
    var value: Int {
        didSet {
            print("值已改变,执行响应式操作")
            // 这里可以执行如更新 UI 等响应式操作
        }
    }

    init(value: Int) {
        self.value = value
    }
}

let reactiveObject = ReactiveValue(value: 0)
reactiveObject.value = 1

在这个 ReactiveValue 类中,value 属性的 didSet 观察者在值改变时执行响应式操作,模拟了响应式编程中对数据变化的响应。

通过深入理解 Swift 的属性观察者和键值编码,并将它们与不同的编程范式结合应用,可以编写出更加灵活、高效和可维护的代码。无论是在小型应用还是大型项目中,这些特性都能为开发者提供强大的工具来处理复杂的业务逻辑和数据管理。同时,在使用过程中要注意性能和类型安全等问题,以确保代码的质量和稳定性。