在 Xcode 11 beta 1 中,Swift 中使用的修飾符名字是
@propertyDelegate
,如今的名稱爲@propertyWrapper
。git
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
在 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
。
@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 }
}
複製代碼
因爲展開後 _firstName
是 private
, 外部沒法訪問到這個屬性,沒法使用他提供方法。
@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 }
}
複製代碼
projectedValue
叫作 Projection properties
(投影屬性),所以 firstName 的投影屬性是 $firstName
,firstName 可見的位置它也均可見。 投影屬性以 $
爲前綴。
有了投影屬性,咱們能夠愉快的使用下面的操做了
somePerson.firstName = "Taylor"
$somePerson.flush()
複製代碼
在 vapor/fluent-kit 的 1.0.0-alpha.3
中已大量使用該特性。
爲了加深對屬性包裝器的瞭解,咱們繼續看幾個樣例。
@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
}
}
}
複製代碼
@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
}
}
}
複製代碼
@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
}
複製代碼
@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)
}
}
}
複製代碼
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
- 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
Property Wrapper 簡化代碼是毋庸置疑的,在使用方面,咱們能夠自定義出各類訪問策略,有更多的想象空間。由於這些策略能夠說是對數據存儲的約束,那麼代碼的健壯性,安全性也將提升。
更多閱讀,請關注 SwiftOldBird 官方微信公衆號