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

在 Xcode 11 beta 1 中,Swift 中使用的修飾符名字是 @propertyDelegate,如今的名稱爲 @propertyWrappergit

0258-property-wrappersgithub

SwiftUI 中幾個常見的 @ 開頭修飾,如 @State@Binding@Environment@EnvironmentObject 等都是運用了 Property Wrappers 這個特性。數據庫

Property Wrappers 特性使得代碼更加簡潔可讀,減小模板代碼,用戶能夠靈活自定義。swift

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

分析

@Lazy 的實現

swift 5.1 版本之前,若是要使用惰性初始化一個屬性,須要在屬性添加 lazy 關鍵字,而 lazy 的處理是在編譯的時候將一些固定模式的硬編碼嵌入進去,其支持的範圍可想而知。安全

lazy 來聲明一個惰性初始化的屬性微信

lazy var foo = 1738
複製代碼

若是沒有語言層面的支持,須要寫大量以下的樣板代碼得到相同的效果:app

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,能夠簡單的聲明爲fetch

@Lazy var foo = 1738
複製代碼

@Lazy 與 lazy 的使用是比較相像,都很簡單明瞭。ui

那麼 @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 官方微信公衆號

相關文章
相關標籤/搜索