Swift Property Wrappers 深度解析

79 阅读6分钟

原文:xuanhu.info/projects/it…

Swift Property Wrappers 深度解析

Swift 5.1 引入了 Property Wrappers(属性包装器),这是一个强大的特性,允许开发者以声明式的方式抽象属性行为的通用模式。通过将通用逻辑封装到可复用的包装器中,可以显著减少样板代码,提高代码的可读性和可维护性。本文将深入探讨 Property Wrappers 的各个方面,包括其基本概念、工作原理、常见用例、高级特性以及当前的一些限制。

1. 什么是 Property Wrappers?

Property Wrappers 是一种用于封装属性存储和访问逻辑的机制。它们允许开发者将常见的属性行为(如验证、转换、存储)抽象出来,并通过简单的注解应用到多个属性上。本质上,Property Wrappers 提供了一种可重用的方式来定义属性的 getter 和 setter 行为,而无需在每个属性中重复相同的代码。

1.1 基本语法

要定义一个 Property Wrapper,你需要使用 @propertyWrapper 属性来标记一个结构体、枚举或类。这个类型必须包含一个名为 wrappedValue 的属性,它定义了包装值的存储和访问方式。


@propertyWrapper

struct Capitalized {

private var value: String = ""

var wrappedValue: String {

get { value }

set { value = newValue.capitalized }

}

init(wrappedValue: String) {

self.wrappedValue = wrappedValue

}

}

在这个例子中,Capitalized 是一个 Property Wrapper,它会将字符串值首字母大写。你可以这样使用它:


struct User {

@Capitalized var name: String

}

  


var user = User()

user.name = "john doe"

print(user.name) // 输出:"John Doe"

2. Property Wrappers 的工作原理

当你使用 @Capitalized 修饰一个属性时,编译器会自动将该属性的访问重定向到 Property Wrapper 的 wrappedValue。这意味着对属性的读取和写入操作实际上是通过 wrappedValue 的 getter 和 setter 来完成的。

2.1 编译器的魔法

编译器会为被包装的属性生成一些额外的代码。例如,对于 @Capitalized var name: String,编译器实际上会生成一个名为 _name 的存储属性,其类型为 Capitalized,以及一个名为 name 的计算属性,该属性委托给 _name.wrappedValue

3. 常见用例

Property Wrappers 可以应用于多种场景,以下是一些常见的用例。

3.1 值验证

确保属性值始终在特定范围内是一个常见需求。例如,限制一个分数在 0 到 100 之间。


@propertyWrapper

struct Clamped<Value: Comparable> {

private var value: Value

let range: ClosedRange<Value>

var wrappedValue: Value {

get { value }

set { value = min(max(range.lowerBound, newValue), range.upperBound) }

}

init(wrappedValue: Value, _ range: ClosedRange<Value>) {

self.value = min(max(range.lowerBound, wrappedValue), range.upperBound)

self.range = range

}

}

  


struct Player {

@Clamped(0...100) var score: Int = 0

}

  


var player = Player()

player.score = 150

print(player.score) // 输出:100

3.2 UserDefaults 封装

使用 Property Wrappers 可以简化 UserDefaults 的访问,避免重复的样板代码。


@propertyWrapper

struct UserDefault<T> {

let key: String

let defaultValue: T

var storage: UserDefaults = .standard

var wrappedValue: T {

get {

storage.object(forKey: key) as? T ?? defaultValue

}

set {

if let optional = newValue as? AnyOptional, optional.isNil {

storage.removeObject(forKey: key)

} else {

storage.set(newValue, forKey: key)

}

}

}

init(wrappedValue defaultValue: T, key: String, storage: UserDefaults = .standard) {

self.defaultValue = defaultValue

self.key = key

self.storage = storage

}

}

  


// 支持可选值的协议

protocol AnyOptional {

var isNil: Bool { get }

}

  


extension Optional: AnyOptional {

var isNil: Bool { self == nil }

}

使用方式:


extension UserDefaults {

@UserDefault(key: "username", defaultValue: "Guest")

static var username: String

@UserDefault(key: "year_of_birth")

static var yearOfBirth: Int?

}

  


UserDefaults.username = "Alice"

UserDefaults.yearOfBirth = 1990

UserDefaults.yearOfBirth = nil // 会自动从 UserDefaults 中移除该键

3.3 观察值变化

通过 Property Wrappers,可以轻松实现属性值的观察。


@propertyWrapper

struct Observable<T> {

private var value: T

private var observer: ((T) -> Void)?

var wrappedValue: T {

get { value }

set {

value = newValue

observer?(value)

}

}

var projectedValue: Observable<T> { self }

mutating func bind(observer: @escaping (T) -> Void) {

self.observer = observer

}

init(wrappedValue: T) {

self.value = wrappedValue

}

}

  


class UserProfile {

@Observable var name: String = "" {

didSet {

print("Name changed to \(name)")

}

}

}

3.4 字符串处理

处理用户输入时,经常需要修剪字符串两端的空白字符。


@propertyWrapper

struct Trimmed {

private(set) var value: String = ""

var wrappedValue: String {

get { value }

set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }

}

init(wrappedValue: String) {

self.wrappedValue = wrappedValue

}

}

  


struct Post {

@Trimmed var title: String

@Trimmed var body: String

}

  


let post = Post(title: " Swift Property Wrappers ", body: "...")

print(post.title) // 输出:"Swift Property Wrappers"

4. 高级特性

4.1 投影值(Projected Values)

Property Wrappers 可以通过 projectedValue 提供额外的功能。使用 $ 前缀可以访问投影值。


@propertyWrapper

struct Clamped<Value: Comparable> {

private var value: Value

let range: ClosedRange<Value>

var wrappedValue: Value {

get { value }

set { value = min(max(range.lowerBound, newValue), range.upperBound) }

}

var projectedValue: ClosedRange<Value> { range }

init(wrappedValue: Value, _ range: ClosedRange<Value>) {

self.value = min(max(range.lowerBound, wrappedValue), range.upperBound)

self.range = range

}

}

  


struct Temperature {

@Clamped(0...100) var celsius: Double

}

  


var temp = Temperature(celsius: 25)

print(temp.celsius) // 输出:25.0

print(temp.$celsius) // 输出:0.0...100.0(范围)

4.2 组合多个 Property Wrappers

可以在单个属性上应用多个 Property Wrappers,以组合它们的行为。注意,它们的应用顺序是从内到外的。


@propertyWrapper

struct Trimmed {

private(set) var value: String = ""

var wrappedValue: String {

get { value }

set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }

}

init(wrappedValue: String) {

self.wrappedValue = wrappedValue

}

}

  


@propertyWrapper

struct Capitalized {

private var value: String = ""

var wrappedValue: String {

get { value }

set { value = newValue.capitalized }

}

init(wrappedValue: String) {

self.wrappedValue = wrappedValue

}

}

  


struct User {

@Trimmed @Capitalized var name: String

}

  


var user = User(name: " john doe ")

print(user.name) // 输出:"John Doe"

4.3 在函数参数中使用 Property Wrappers

Property Wrappers 也可以用于函数参数,为参数提供验证或转换逻辑。


@propertyWrapper

struct Positive {

var wrappedValue: Int

init(wrappedValue: Int) {

if wrappedValue < 0 {

self.wrappedValue = 0

} else {

self.wrappedValue = wrappedValue

}

}

}

  


func doSomething(@Positive value: Int) {

print("Value is \(value)")

}

  


doSomething(value: 42) // 输出:Value is 42

doSomething(value: -5) // 输出:Value is 0

另一个例子,格式化函数参数:


@propertyWrapper

struct Uppercase {

var wrappedValue: String

init(wrappedValue: String) {

self.wrappedValue = wrappedValue.uppercased()

}

}

  


func greet(@Uppercase name: String) {

print("Hello, \(name)!")

}

  


greet(name: "John") // 输出:Hello, JOHN!

4.4 访问包装器实例

使用 _ 前缀可以直接访问 Property Wrapper 的实例,而 $ 前缀用于访问投影值。


extension UserDefaults {

@UserDefault(key: "username", defaultValue: "Guest")

static var username: String

static func debugKeys() {

print(_username.key) // 输出:"username"

print($username) // 输出:投影值(如果有)

}

}

5. 实际应用案例

5.1 在 SwiftUI 中管理用户偏好设置

在 macOS 或 iOS 应用中,使用 Property Wrappers 可以非常优雅地管理用户偏好设置。


import SwiftUI

  


class Settings: ObservableObject {

@UserDefault(key: "username", defaultValue: "Guest")

var username: String

@UserDefault(key: "isDarkMode", defaultValue: false)

var isDarkMode: Bool

}

  


struct ContentView: View {

@StateObject private var settings = Settings()

var body: some View {

Form {

Section(header: Text("User Settings")) {

TextField("Username", text: $settings.username)

Toggle("Dark Mode", isOn: $settings.isDarkMode)

}

}

.padding()

.frame(width: 300, height: 200)

}

}

  


@main

struct MyApp: App {

var body: some Scene {

WindowGroup {

ContentView()

.preferredColorScheme(settings.isDarkMode ? .dark : .light)

}

}

}

5.2 与 Combine 框架集成

通过投影值,Property Wrappers 可以很容易地与 Combine 框架集成,提供响应式能力。


import Combine

  


@propertyWrapper

struct UserDefault<Value> {

private let key: String

private let defaultValue: Value

private let publisher = PassthroughSubject<Value, Never>()

private var storage: UserDefaults = .standard

var wrappedValue: Value {

get {

storage.object(forKey: key) as? Value ?? defaultValue

}

set {

if let optional = newValue as? AnyOptional, optional.isNil {

storage.removeObject(forKey: key)

} else {

storage.set(newValue, forKey: key)

}

publisher.send(newValue)

}

}

var projectedValue: AnyPublisher<Value, Never> {

publisher.eraseToAnyPublisher()

}

init(wrappedValue defaultValue: Value, key: String, storage: UserDefaults = .standard) {

self.defaultValue = defaultValue

self.key = key

self.storage = storage

}

}

  


// 订阅变化

let cancellable = UserDefaults.$username.sink {

print("用户名变为:\($0)")

}

  


UserDefaults.username = "新名字"

// 控制台输出:用户名变为:新名字

6. 限制与注意事项

尽管 Property Wrappers 非常强大,但它们也有一些限制和需要注意的地方。

6.1 当前限制

  • 不能用于 lazy 属性:Property Wrappers 不能应用于标记为 lazy 的属性。

  • 不能在协议声明中使用:不能在协议中定义带有 Property Wrappers 的属性。

  • 不能抛出错误:Property Wrappers 无法让属性抛出错误,处理无效值的方式有限(如忽略或使用 fatalError())。

  • 多个包装器的顺序问题:当多个 Property Wrappers 应用于单个属性时,顺序很重要,因为它们是从内到外应用的。

  • 不能与某些属性修饰符共用:不能与 @NSCopying@NSManagedweakunowned 等修饰符一起使用。

  • 类型别名限制:带有包装器的属性不能用 typealias 标记。

6.2 处理复杂场景的变通方案

由于目前不支持直接组合多个 Property Wrappers,可以通过嵌套的方式来实现类似功能。


@propertyWrapper

struct Dasherized {

private(set) var value: String = ""

var wrappedValue: String {

get { value }

set { value = newValue.replacingOccurrences(of: " ", with: "-") }

}

init(wrappedValue: String) {

self.wrappedValue = wrappedValue

}

}

  


// 通过嵌套实现多个功能

@propertyWrapper

struct TrimmedAndDasherized {

@Trimmed private var trimmedValue: String

@Dasherized private var dasherizedValue: String

var wrappedValue: String {

get { dasherizedValue }

set {

trimmedValue = newValue

dasherizedValue = trimmedValue

}

}

init(wrappedValue: String) {

self.wrappedValue = wrappedValue

}

}

  


struct Post {

@TrimmedAndDasherized var slug: String

}

7. 总结

Swift 的 Property Wrappers 是一个极具表现力的功能,它允许开发者将属性的通用访问模式抽象成可重用的组件。通过减少样板代码,它们使代码更加简洁、可读和可维护。从值验证到 UserDefaults 封装,从字符串处理到与 Combine 的响应式集成,Property Wrappers 的应用场景广泛且实用。

虽然目前存在一些限制,如不能用于 lazy 属性或协议中,但通过一些巧妙的变通方案,仍然可以在大多数场景中享受其带来的便利。随着 Swift 语言的不断发展,相信 Property Wrappers 的功能会变得更加强大和灵活。

原文:xuanhu.info/projects/it…