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

Swift属性包装器深入解析

2024-02-137.7k 阅读

什么是Swift属性包装器

属性包装器是Swift 5.1引入的一个重要特性,它允许开发者以一种简洁且可复用的方式来管理属性的存储和访问。通过属性包装器,我们可以将属性的逻辑与属性本身分离,从而提高代码的可维护性和复用性。

从本质上来说,属性包装器就是一种结构体、类或枚举,它们需要遵循 PropertyWrapper 协议。这个协议要求属性包装器必须包含一个 wrappedValue 属性,用于存储被包装属性的实际值。此外,属性包装器还可以提供一个 projectedValue 属性,用于提供一些额外的功能或信息。

基本语法

要定义一个属性包装器,我们首先需要定义一个遵循 PropertyWrapper 协议的类型。例如,下面是一个简单的属性包装器,它用于对属性值进行简单的验证:

@propertyWrapper
struct NonNegative {
    private var value: Int
    init() {
        self.value = 0
    }
    var wrappedValue: Int {
        get { value }
        set {
            if newValue >= 0 {
                value = newValue
            } else {
                print("不能设置为负数")
            }
        }
    }
}

在上面的代码中,NonNegative 结构体遵循了 PropertyWrapper 协议,它包含一个私有属性 value 用于存储实际的值,以及一个 wrappedValue 属性用于对外暴露属性值,并在设置值时进行验证。

使用这个属性包装器也非常简单,我们只需要在属性声明前加上 @NonNegative 即可:

class SomeClass {
    @NonNegative var number: Int
    init() {
        self.number = 5
    }
}

let instance = SomeClass()
print(instance.number) // 输出: 5

instance.number = -3 // 输出: 不能设置为负数

初始化属性包装器

属性包装器可以有多种初始化方式。我们可以在定义属性包装器时提供默认的初始化器,就像上面 NonNegative 例子中的那样。此外,我们还可以为属性包装器提供带参数的初始化器。

例如,我们可以修改 NonNegative 属性包装器,使其可以接受一个初始值:

@propertyWrapper
struct NonNegative {
    private var value: Int
    init(wrappedValue: Int) {
        if wrappedValue >= 0 {
            self.value = wrappedValue
        } else {
            self.value = 0
        }
    }
    var wrappedValue: Int {
        get { value }
        set {
            if newValue >= 0 {
                value = newValue
            } else {
                print("不能设置为负数")
            }
        }
    }
}

class AnotherClass {
    @NonNegative(wrappedValue: 10) var number: Int
}

let anotherInstance = AnotherClass()
print(anotherInstance.number) // 输出: 10

在上面的代码中,NonNegative 属性包装器提供了一个接受 wrappedValue 参数的初始化器。在使用 @NonNegative(wrappedValue: 10) 时,会调用这个初始化器来初始化属性包装器。

projectedValue 的使用

除了 wrappedValue,属性包装器还可以提供一个 projectedValue 属性。这个属性可以用于提供一些额外的功能或信息。

例如,我们可以定义一个属性包装器,它不仅可以存储属性值,还可以记录属性被访问和修改的次数:

@propertyWrapper
struct Tracked {
    private var value: Int
    private var accessCount = 0
    private var modificationCount = 0
    init(wrappedValue: Int) {
        self.value = wrappedValue
    }
    var wrappedValue: Int {
        get {
            accessCount += 1
            return value
        }
        set {
            modificationCount += 1
            value = newValue
        }
    }
    var projectedValue: (accessCount: Int, modificationCount: Int) {
        return (accessCount, modificationCount)
    }
}

class TrackedClass {
    @Tracked(wrappedValue: 5) var number: Int
}

let trackedInstance = TrackedClass()
print(trackedInstance.number) // 访问次数加1
trackedInstance.number = 10 // 修改次数加1

let projection = trackedInstance.$number
print(projection.accessCount) // 输出: 1
print(projection.modificationCount) // 输出: 1

在上面的代码中,Tracked 属性包装器提供了一个 projectedValue 属性,它返回一个包含属性访问次数和修改次数的元组。通过 $number 可以访问到 projectedValue

属性包装器的继承与复用

属性包装器可以像普通的结构体、类或枚举一样被继承和复用。这使得我们可以基于现有的属性包装器创建更复杂的功能。

例如,我们可以创建一个继承自 NonNegative 的属性包装器,它不仅保证属性值非负,还限制属性值不能超过某个最大值:

@propertyWrapper
struct LimitedNonNegative: NonNegative {
    private let maxValue: Int
    init(wrappedValue: Int, max: Int) {
        self.maxValue = max
        super.init(wrappedValue: wrappedValue)
    }
    override var wrappedValue: Int {
        get { super.wrappedValue }
        set {
            if newValue <= maxValue {
                super.wrappedValue = newValue
            } else {
                print("超过最大值 \(maxValue)")
            }
        }
    }
}

class LimitedClass {
    @LimitedNonNegative(wrappedValue: 5, max: 10) var number: Int
}

let limitedInstance = LimitedClass()
limitedInstance.number = 15 // 输出: 超过最大值 10

在上面的代码中,LimitedNonNegative 结构体继承自 NonNegative,并添加了一个 maxValue 属性来限制属性的最大值。通过重写 wrappedValue 的 setter 方法,实现了对属性值的进一步限制。

在类和结构体中的使用差异

属性包装器在类和结构体中的行为有一些细微的差异。由于结构体是值类型,而类是引用类型,这会影响到属性包装器的存储和传递方式。

在结构体中,属性包装器的值是结构体实例的一部分。这意味着当结构体实例被传递或复制时,属性包装器也会被复制。

struct StructWithWrapper {
    @NonNegative var number: Int
}

var structInstance1 = StructWithWrapper()
structInstance1.number = 10

var structInstance2 = structInstance1
structInstance2.number = 20

print(structInstance1.number) // 输出: 10

在上面的代码中,structInstance1structInstance2 是两个不同的结构体实例,它们的 number 属性相互独立。

而在类中,属性包装器是作为类实例的一部分存储在堆上的。当类实例被传递时,实际上传递的是引用。

class ClassWithWrapper {
    @NonNegative var number: Int
}

let classInstance1 = ClassWithWrapper()
classInstance1.number = 10

let classInstance2 = classInstance1
classInstance2.number = 20

print(classInstance1.number) // 输出: 20

在上面的代码中,classInstance1classInstance2 指向同一个类实例,因此对 classInstance2number 属性的修改会影响到 classInstance1

与其他Swift特性的结合

  1. 与计算属性的结合 属性包装器可以与计算属性一起使用,为计算属性提供额外的功能。例如,我们可以创建一个属性包装器,用于缓存计算属性的结果:
@propertyWrapper
struct Cached {
    private var value: Int?
    private var closure: () -> Int
    init(_ closure: @escaping () -> Int) {
        self.closure = closure
    }
    var wrappedValue: Int {
        if let value = value {
            return value
        } else {
            let result = closure()
            value = result
            return result
        }
    }
}

class CachedClass {
    @Cached {
        // 这里可以是复杂的计算逻辑
        return 1 + 2 + 3
    } var cachedResult: Int
}

let cachedInstance = CachedClass()
print(cachedInstance.cachedResult) // 第一次计算并缓存
print(cachedInstance.cachedResult) // 直接返回缓存的值

在上面的代码中,Cached 属性包装器用于缓存计算属性的结果。通过闭包来定义计算逻辑,只有在第一次访问 wrappedValue 时才会执行闭包,后续访问直接返回缓存的值。

  1. 与协议的结合 属性包装器可以与协议结合,为遵循协议的类型提供统一的属性管理方式。例如,我们可以定义一个协议,要求遵循该协议的类型必须有一个非负的 count 属性:
protocol Countable {
    @NonNegative var count: Int { get set }
}

class CountableClass: Countable {
    @NonNegative var count: Int
    init() {
        self.count = 0
    }
}

在上面的代码中,Countable 协议定义了一个 @NonNegative 修饰的 count 属性。CountableClass 遵循该协议,并实现了 count 属性,确保其值始终非负。

在实际项目中的应用场景

  1. 数据验证与转换 在处理用户输入或从外部数据源获取的数据时,经常需要对数据进行验证和转换。属性包装器可以方便地实现这些功能。例如,在一个用户注册的场景中,我们可能需要验证用户输入的年龄是否为非负数:
@propertyWrapper
struct ValidAge {
    private var value: Int
    init() {
        self.value = 0
    }
    var wrappedValue: Int {
        get { value }
        set {
            if newValue >= 0 && newValue <= 120 {
                value = newValue
            } else {
                print("年龄不合法")
            }
        }
    }
}

class User {
    @ValidAge var age: Int
    init() {
        self.age = 0
    }
}

let user = User()
user.age = 25
user.age = -5 // 输出: 年龄不合法
  1. 依赖注入 在一些复杂的项目中,依赖注入是一种常见的设计模式。属性包装器可以用于简化依赖注入的过程。例如,我们可以创建一个属性包装器,用于注入网络服务:
protocol NetworkService {
    func fetchData() -> String
}

class DefaultNetworkService: NetworkService {
    func fetchData() -> String {
        return "默认数据"
    }
}

@propertyWrapper
struct InjectedNetworkService {
    private var service: NetworkService
    init(_ service: NetworkService) {
        self.service = service
    }
    var wrappedValue: NetworkService {
        return service
    }
}

class ViewModel {
    @InjectedNetworkService(DefaultNetworkService()) var networkService: NetworkService
    func loadData() {
        let data = networkService.fetchData()
        print(data)
    }
}

let viewModel = ViewModel()
viewModel.loadData() // 输出: 默认数据
  1. 日志记录与调试 在开发过程中,日志记录和调试是非常重要的。属性包装器可以用于自动记录属性的访问和修改操作,方便调试。例如,我们可以创建一个属性包装器,用于记录属性的修改日志:
@propertyWrapper
struct Logged {
    private var value: Int
    init(wrappedValue: Int) {
        self.value = wrappedValue
    }
    var wrappedValue: Int {
        get { value }
        set {
            print("将值从 \(value) 修改为 \(newValue)")
            value = newValue
        }
    }
}

class LoggedClass {
    @Logged(wrappedValue: 5) var number: Int
}

let loggedInstance = LoggedClass()
loggedInstance.number = 10 // 输出: 将值从 5 修改为 10

注意事项与常见问题

  1. 属性包装器与存储属性的关系 属性包装器本身并不直接存储被包装的属性,而是通过 wrappedValue 来间接管理属性值。这意味着在访问和修改属性时,实际上是在操作 wrappedValue。同时,属性包装器的存储和生命周期与包含它的类型相关。

  2. 初始化顺序 在使用属性包装器时,需要注意初始化顺序。当一个类型包含多个属性包装器时,它们的初始化顺序与声明顺序一致。此外,如果属性包装器的初始化依赖于其他属性的值,可能会导致编译错误或意外的行为。

  3. 与KVO(Key - Value Observing)的兼容性 虽然属性包装器提供了一种强大的属性管理方式,但在与KVO结合使用时需要注意。由于属性包装器改变了属性的访问方式,可能会影响KVO的正常工作。在需要使用KVO的场景中,可能需要额外的处理来确保属性的变化能够被正确观察到。

  4. 性能考虑 虽然属性包装器提供了很多便利,但在性能敏感的场景中,需要注意其带来的额外开销。例如,每次访问和修改 wrappedValue 可能会涉及到方法调用,这可能会对性能产生一定的影响。在这种情况下,需要权衡功能和性能之间的关系。

通过深入了解Swift属性包装器的各个方面,包括定义、使用、与其他特性的结合以及在实际项目中的应用场景,开发者可以更好地利用这一特性来提高代码的质量和可维护性。同时,注意在使用过程中的各种细节和潜在问题,能够避免不必要的错误和性能问题。属性包装器为Swift开发者提供了一种强大而灵活的工具,使得属性管理变得更加优雅和高效。