@propertyWrapper属性的四个潜在用例:

  • Constraining Values
  • Transforming Values on Property Assignment
  • Changing Synthesized Equality and Comparison Semantics :
  • Auditing Property Access

Constraining Values

Implementing a value clamping property wrapper

@propertyWrapper
struct Clamping<Value: Comparable> {
    var value: Value
    let range: ClosedRange<Value>

    init(initialValue value: Value, _ range: ClosedRange<Value>) {
        precondition(range.contains(value))
        self.value = value
        self.range = range
    }

    var wrappedValue: Value {
        get { value }
        set { value = min(max(range.lowerBound, newValue), range.upperBound) }
    }
}


您可以使用@ clamp来保证在化学溶液中模拟酸度的性质在传统的0 - 14范围内。

struct Solution {
    @Clamping(0...14) var pH: Double = 7.0
}

试图将pH值设置在该范围之外,会导致使用最近的边界值(最小值或最大值)。

let carbonicAcid = Solution(pH: 4.68) // at 1 mM under standard conditions

let superDuperAcid = Solution(pH: -1)
superDuperAcid.pH // 0

您可以在其他属性包装器的实现中使用属性包装器。


@propertyWrapper
struct UnitInterval<Value: FloatingPoint> {
    @Clamping(0...1)
    var wrappedValue: Value = .zero

    init(initialValue value: Value) {
        self.wrappedValue = value
    }
}

例如,您可以使用@UnitInterval属性包装器来定义一个RGB类型,以百分比表示红、绿、蓝的强度。

struct RGB {
    @UnitInterval var red: Double
    @UnitInterval var green: Double
    @UnitInterval var blue: Double
}

let cornflowerBlue = RGB(red: 0.392, green: 0.584, blue: 0.929)

Transforming Values on Property Assignment

接受用户的文本输入一直是应用开发者头疼的问题。要跟踪的东西太多了,从普通的字符串编码到通过文本字段注入代码的恶意尝试。

import Foundation

URL(string: " https://nshipster.com") // nil (!)

ISO8601DateFormatter().date(from: " 2019-06-24") // nil (!)

let words = " Hello, world!".components(separatedBy: .whitespaces)
words.count // 3 (!)

尝试改进,你不能用它来改变已经在运行的事件。😓

struct Post {
    var title: String {
        willSet {
            title = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
            /* ⚠️ Attempting to store to property 'title' within its own willSet,which is about to be overwritten by the new value   */
        }
    }
}

再次改进,didSet在初始属性分配期间没有被调用。😓

struct Post {
    var title: String {
        // 😓 Not called during initialization
        didSet {
            self.title = title.trimmingCharacters(in: .whitespacesAndNewlines)
        }
    }
}

好在在它自己的didSet回调中设置属性不会导致回调再次触发,所以您不必担心无限自递归。

Implementing a Property Wrapper that Trims Whitespace from String Values

使用属性包装器改进

import Foundation

@propertyWrapper
struct Trimmed {
    private(set) var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
    }

    init(initialValue: String) {
        self.wrappedValue = initialValue
    }
}

无论是在初始化过程中,还是之后通过属性访问-自动删除其开头或结尾的空格。

struct Post {
    @Trimmed var title: String
    @Trimmed var body: String
}

let quine = Post(title: "  Swift Property Wrappers  ", body: "…")
quine.title // "Swift Property Wrappers" (no leading or trailing spaces!)

quine.title = "      @propertyWrapper     "
quine.title // "@propertyWrapper" (still no leading or trailing spaces!)

Changing Synthesized Equality and Comparison Semantics

如果您的特定用例需要不同的相等语义呢?假设你想要一个不区分大小写的字符串相等的概念?

Implementing a case-insensitive property wrapper

import Foundation

@propertyWrapper
struct CaseInsensitive<Value: StringProtocol> {
    var wrappedValue: Value
}

extension CaseInsensitive: Comparable {
    private func compare(_ other: CaseInsensitive) -> ComparisonResult {
        wrappedValue.caseInsensitiveCompare(other.wrappedValue)
    }

    static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {
        lhs.compare(rhs) == .orderedSame
    }

    static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {
        lhs.compare(rhs) == .orderedAscending
    }

    static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {
        lhs.compare(rhs) == .orderedDescending
    }
}

构造两个仅因大小写不同的字符串值,它们将在标准的相等性检查中返回false,但在不区分大小写的对象中包装时返回true。

let hello: String = "hello"
let HELLO: String = "HELLO"

hello == HELLO // false
CaseInsensitive(wrappedValue: hello) == CaseInsensitive(wrappedValue: HELLO) // true

属性包装器允许我们完全放弃所有这些繁忙的工作:

struct Account: Equatable {
   @CaseInsensitive var name: String

   init(name: String) {
       $name = CaseInsensitive(wrappedValue: name)
   }
}

var johnny = Account(name: "johnny")
let JOHNNY = Account(name: "JOHNNY")
let Jane = Account(name: "Jane")

johnny == JOHNNY // true
johnny == Jane // false

johnny.name == JOHNNY.name // false

johnny.name = "Johnny"
johnny.name // "Johnny"

Since Swift 4, the compiler automatically synthesizes Equatable conformance to types that adopt it in their declaration and whose stored properties are all themselves Equatable.

Synthesized by Swift Compiler

extension Account: Equatable {
    static func == (lhs: Account, rhs: Account) -> Bool {
        lhs.$name == rhs.$name
    }
}

Auditing Property Access

Implementing a Property Value Versioning

import Foundation

@propertyWrapper
struct Versioned<Value> {
    private var value: Value
    private(set) var timestampedValues: [(Date, Value)] = []

    var wrappedValue: Value {
        get { value }

        set {
            defer { timestampedValues.append((Date(), value)) }
            value = newValue
        }
    }

    init(initialValue value: Value) {
        self.wrappedValue = value
    }
}

假设的ExpenseReport类可以用@Versioned注释包装其state属性,以便在处理过程中为每个操作保留书面记录。

class ExpenseReport {
    enum State { case submitted, received, approved, denied }

    @Versioned var state: State = .submitted
}

Properties can’t be marked as throws.

如果不能参与错误处理,属性包装器就不能提供一种合理的方式来执行和传递策略。 例如,如果我们想扩展之前的@Versioned属性包装器,以防止在.denied之后将状态设置为.approved,我们最好的选择是fatalError(),它并不适合真正的应用:

class ExpenseReport {
    @Versioned var state: State = .submitted {
        willSet {
            if newValue == .approved,
                $state.timestampedValues.map { $0.1 }.contains(.denied)
            {
                fatalError("J'Accuse!")
            }
        }
    }
}

var tripExpenses = ExpenseReport()
tripExpenses.state = .denied
tripExpenses.state = .approved // Fatal error: "J'Accuse!"

这只是到目前为止我们在使用属性包装时遇到的几个限制之一。

Limitations

Properties Can’t Participate in Error Handling - 属性不能参与错误处理

属性包装器只有两种方法来处理无效值:

  • Ignoring them (silently)
  • Crashing with fatalError()

Wrapped Properties Can’t Be Aliased - 包装的属性不能起别名

不能使用属性包装器的实例作为属性包装器。

typealias UnitInterval = Clamping(0...1) // ❌

let UnitInterval = Clamping(0...1)
struct Solution { @UnitInterval var pH: Double } // ❌

Property Wrappers Are Difficult To Compose - 属性包装器组合困难

属性包装器的组合不是一个交换操作;声明它们的顺序会影响它们的行为。最外层的包装器作用于最内层包装器类型的值。

@propertyWrapper
struct Dasherized {
    private(set) var value: String = ""

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

    init(initialValue: String) {
        self.wrappedValue = initialValue
    }
}

struct Post {
    …
    @Dasherized @Trimmed var slug: String // ⚠️ An internal error occurred.
}

Property Wrappers Aren’t First-Class Dependent Types - 属性包装器不是一等依赖类型

依赖类型是由其值定义的类型。例如,“一对整数,其中后者大于前者”和“具有素数元素的数组”都是依赖类型,因为它们的类型定义取决于其值。
Swift在其类型系统中缺乏对依赖类型的支持,这意味着任何此类保证都必须在运行时强制执行。

您不能使用属性包装来定义一个带有约束的新类型

typealias pH = @Clamping(0...14) Double // ❌
func acidity(of: Chemical) -> pH {}

也不能使用属性包装器来注释集合中的键或值类型。

enum HTTP {
    struct Request {
        var headers: [@CaseInsensitive String: String] // ❌
    }
}

Property Wrappers Are Difficult to Document - 属性包装很难记录

Property Wrappers Further Complicate Swift - 属性包装使Swift更加复杂

更多文章:

Swift Property Wrappers

Property wrappers in Swift

使用 Property Wrapper 为 Codable 解码设定默认值