在 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 官方微信公众号