阅读 927

Swift5.1 不得不知的 Property Wrappers 特性

在 Xcode 11 beta 1 中,Swift 中使用的修饰符名字是 @propertyDelegate,现在的名称为 @propertyWrapper

0258-property-wrappers

SwiftUI 中几个常见的 @ 开头修饰,如 @State@Binding@Environment@EnvironmentObject 等都是运用了 Property Wrappers 这个特性。

Property Wrappers 特性使得代码更加简洁可读,减少模板代码,用户可以灵活自定义。

We saw the future of Swift, and it was full of @s.

分析

@Lazy 的实现

swift 5.1 版本以前,如果要使用惰性初始化一个属性,需要在属性添加 lazy 关键字,而 lazy 的处理是在编译的时候将一些固定模式的硬编码嵌入进去,其支持的范围可想而知。

lazy 来声明一个惰性初始化的属性

lazy var foo = 1738
复制代码

如果没有语言层面的支持,需要写大量如下的样板代码获得相同的效果:

struct Foo {
  private var _foo: Int?
  var foo: Int {
    get {
      if let value = _foo { return value }
      let initialValue = 1738
      _foo = initialValue
      return initialValue
    }
    set {
      _foo = newValue
    }
  }
}
复制代码

通过 property wrappers,可以简单的声明为

@Lazy var foo = 1738
复制代码

@Lazy 与 lazy 的使用是比较相像,都很简单明了。

那么 @Lazy 这个属性包装器类型是如何实现的?

@propertyWrapper
enum Lazy<Value> {
  case uninitialized(() -> Value)
  case initialized(Value)

  init(wrappedValue: @autoclosure @escaping () -> Value) {
    self = .uninitialized(wrappedValue)
  }

  var wrappedValue: Value {
    mutating get {
      switch self {
      case .uninitialized(let initializer):
        let value = initializer()
        self = .initialized(value)
        return value
      case .initialized(let value):
        return value
      }
    }
    set {
      self = .initialized(newValue)
    }
  }
}
复制代码

属性包装器类型为使用它作为包装器的 属性 提供存储。 wrappedValue 计算属性提供了包装器真正的实现。

@Lazy var foo = 1738
复制代码

会被转换为:

private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
var foo: Int {
  get { return _foo.wrappedValue }
  set { _foo.wrappedValue = newValue }
}
复制代码

我们可以在 Lazy 上提供 reset(_:) 操作,将其设置为新的值:

extension Lazy {
  mutating func reset(_ newValue:  @autoclosure @escaping () -> Value) {
    self = .uninitialized(newValue)
  }
}

_foo.reset(42)
复制代码

我们可以新增一个初始化方法。

extension Lazy {
  init(body: @escaping () -> Value) {
    self = .uninitialized(body)
  }
}

func createAString() -> String { ... }
@Lazy var bar: String  // not initialized yet
_bar = Lazy(body: createAString)
复制代码

上述代码可以等价的声明为单个语句:

@Lazy(body: createAString) var bar: String
复制代码

这时候 @Lazy 可以说已经比 lazy 更加丰富灵活了。

那么属性包装器就只能做这种事情么,为了更好的体会其用法,再分析一个 @File

@Field 的分析

@propertyWrapper
public struct Field<Value: DatabaseValue> {
  public let name: String
  private var record: DatabaseRecord?
  private var cachedValue: Value?
  
  public init(name: String) {
    self.name = name
  }
  
  public func configure(record: DatabaseRecord) {
    self.record = record
  }
  
  public var wrappedValue: Value {
    mutating get {
      if cachedValue == nil { fetch() }
      return cachedValue!
    }
    set {
      cachedValue = newValue
    }
  }
  
  public func flush() {
    if let value = cachedValue {
      record!.flush(fieldName: name, value)
    }
  }
  
  public mutating func fetch() {
    cachedValue = record!.fetch(fieldName: name, type: Value.self)
  }
}

复制代码

我们可以基于 Field 属性包装器定义我们的模型:

public struct Person: DatabaseModel {
  @Field(name: "first_name") public var firstName: String
  @Field(name: "last_name") public var lastName: String
  @Field(name: "date_of_birth") public var birthdate: Date
}
复制代码

File 允许我们刷新现有值,获取新值,并检索数据库中相应的字段的名字。

@Field(name: "first_name") public var firstName: String
复制代码

展开后为:

private var _firstName: Field<String> = Field(name: "first_name")

public var firstName: String {
  get { _firstName.wrappedValue }
  set { _firstName.wrappedValue = newValue }
}
复制代码

由于展开后 _firstNameprivate, 外部无法访问到这个属性,无法使用他提供方法。

@propertyWrapper
public struct Field<Value: DatabaseValue> {
  // ... API as before ...
  // 新增
  public var projectedValue: Self {
    get { self }
    set { self = newValue }
  }
}
复制代码

新增 projectedValue 后,

@Field(name: "first_name") public var firstName: String
复制代码

就会展开为

private var _firstName: Field<String> = Field(name: "first_name")

public var firstName: String {
  get { _firstName.wrappedValue }
  set { _firstName.wrappedValue = newValue }
}

public var $firstName: Field<String> {
  get { _firstName.projectedValue }
  set { _firstName.projectedValue = newValue }
}
复制代码

投影属性(Projection properties)

projectedValue 叫做 Projection properties(投影属性),因此 firstName 的投影属性是 $firstName,firstName 可见的位置它也都可见。 投影属性以 $ 为前缀。

有了投影属性,我们可以愉快的使用下面的操作了

somePerson.firstName = "Taylor"
$somePerson.flush()
复制代码

vapor/fluent-kit1.0.0-alpha.3 中已大量使用该特性。

为了加深对属性包装器的了解,我们继续看几个样例。

举例

延迟初始化(Delayed Initialization)

可变

@propertyWrapper
struct DelayedMutable<Value> {
  private var _value: Value? = nil
  var wrappedValue: Value {
    get {
      guard let value = _value else {
        fatalError("property accessed before being initialized")
      }
      return value
    }
    set {
      _value = newValue
    }
  }

  /// "Reset" the wrapper so it can be initialized again.
  mutating func reset() {
    _value = nil
  }
}
复制代码

不可变

@propertyWrapper
struct DelayedImmutable<Value> {
  private var _value: Value? = nil

  var wrappedValue: Value {
    get {
      guard let value = _value else {
        fatalError("property accessed before being initialized")
      }
      return value
    }

    // Perform an initialization, trapping if the
    // value is already initialized.
    set {
      if _value != nil {
        fatalError("property initialized twice")
      }
      _value = newValue
    }
  }
}
复制代码

NSCopying

@propertyWrapper
struct Copying<Value: NSCopying> {
  private var _value: Value
  init(wrappedValue value: Value) {
    // Copy the value on initialization.
    self._value = value.copy() as! Value
  }
  var wrappedValue: Value {
    get { return _value }
    set {
      // Copy the value on reassignment.
      _value = newValue.copy() as! Value
    }
  }
}
复制代码

User defaults

@propertyWrapper
struct UserDefault<T> {
  let key: String
  let defaultValue: T
  
  var wrappedValue: T {
    get {
      return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
    }
    set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }
}

enum GlobalSettings {
  @UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
  static var isFooFeatureEnabled: Bool
  
  @UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false)
  static var isBarFeatureEnabled: Bool
}
复制代码

AtomicWrite

@propertyWrapper
public struct AtomicWrite<Value> {
    
    // TODO: Faster version with os_unfair_lock?
    
    let queue = DispatchQueue(label: "Atomic write access queue", attributes: .concurrent)
    var storage: Value
    
    public init(initialValue value: Value) {
        self.storage = value
    }
    
    public var wrappedValue: Value {
        get {
            return queue.sync { storage }
        }
        set {
            queue.sync(flags: .barrier) { storage = newValue }
        }
    }
    
    /// Atomically mutate the variable (read-modify-write).
    ///
    /// - parameter action: A closure executed with atomic in-out access to the wrapped property.
    public mutating func mutate(_ mutation: (inout Value) throws -> Void) rethrows {
        return try queue.sync(flags: .barrier) {
            try mutation(&storage)
        }
    }
}
复制代码

Trimmed

public struct Trimmed {
    private var storage: String!
    private let characterSet: CharacterSet
    
    public var wrappedValue: String {
        get { storage }
        set { storage = newValue.trimmingCharacters(in: characterSet) }
    }
    
    public init(initialValue: String) {
        self.characterSet = .whitespacesAndNewlines
        wrappedValue = initialValue
    }
    
    public init(initialValue: String, characterSet: CharacterSet) {
        self.characterSet = characterSet
        wrappedValue = initialValue
    }
}
复制代码
@Trimmed
var text = " \n Hello, World! \n\n    "

print(text) // "Hello, World!"

// By default trims white spaces and new lines, but it also supports any character set
@Trimmed(characterSet: .whitespaces)
var text = " \n Hello, World! \n\n    "
print(text) // "\n Hello, World! \n\n"
复制代码

更多的策略,可以参考 guillermomuntaner/Burritos

Property Wrappers 的一些限制

  • Properties Can’t Participate in Error Handling
  • Wrapped Properties Can’t Be Aliased
  • Property Wrappers Are Difficult To Compose
  • Property Wrappers Aren’t First-Class Dependent Types
  • Property Wrappers Are Difficult to Document
  • Property Wrappers Further Complicate Swift

摘自 Swift Property Wrappers

总结

Property Wrapper 简化代码是毋庸置疑的,在使用方面,我们可以自定义出各种访问策略,有更多的想象空间。因为这些策略可以说是对数据存储的约束,那么代码的健壮性,安全性也将提高。

更多阅读,请关注 SwiftOldBird 官方微信公众号

关注下面的标签,发现更多相似文章
评论