在 Xcode 11 beta 1 中,Swift 中使用的修饰符名字是
@propertyDelegate
,现在的名称为@propertyWrapper
。
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 }
}
由于展开后 _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 }
}
投影属性(Projection properties)
projectedValue
叫做 Projection properties
(投影属性),因此 firstName 的投影属性是 $firstName
,firstName 可见的位置它也都可见。 投影属性以 $
为前缀。
有了投影属性,我们可以愉快的使用下面的操作了
somePerson.firstName = "Taylor"
$somePerson.flush()
在 vapor/fluent-kit 的 1.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
总结
Property Wrapper 简化代码是毋庸置疑的,在使用方面,我们可以自定义出各种访问策略,有更多的想象空间。因为这些策略可以说是对数据存储的约束,那么代码的健壮性,安全性也将提高。
更多阅读,请关注 OldBirds 官方微信公众号