- Proposal: SE-0258
- Authors: Doug Gregor, Joe Groff
- Review Manager: John McCall
- Status: Active Review (June 14th...24th, 2019)
- Implementation: Linux toolchain, macOS toolchain, and master or 5.1 snapshots after June 14, 2019.
- Review: (review #1) (revision announcement #1) (review #2)
- Previous versions: Revision #1
There are property implementation patterns that come up repeatedly. Rather than hardcode a fixed set of patterns into the compiler, we should provide a general "property wrapper" mechanism to allow these patterns to be defined as libraries.
This is an alternative approach to some of the problems intended to be addressed by the 2015-2016 property behaviors proposal. Some of the examples are the same, but this proposal takes a completely different approach designed to be simpler, easier to understand for users, and less invasive in the compiler implementation. There is a section that discusses the substantive differences from that design near the end of this proposal.
We've tried to accommodate several important patterns for properties with
targeted language support, like lazy
and @NSCopying
, but this support has been narrow in scope and utility. For instance, Swift provides lazy
properties as a primitive language feature, since lazy initialization is common and is often necessary to avoid having properties be exposed as Optional
. Without this language support, it takes a lot of boilerplate to get the same effect:
struct Foo {
// lazy var foo = 1738
private var _foo: Int?
var foo: Int {
get {
if let value = _foo { return value }
let initialValue = 1738
_foo = initialValue
return initialValue
}
set {
_foo = newValue
}
}
}
Building lazy
into the language has several disadvantages. It makes the
language and compiler more complex and less orthogonal. It's also inflexible;
there are many variations on lazy initialization that make sense, but we
wouldn't want to hardcode language support for all of them.
There are important property patterns outside of lazy initialization. It often makes sense to have "delayed", once-assignable-then-immutable properties to support multi-phase initialization:
class Foo {
let immediatelyInitialized = "foo"
var _initializedLater: String?
// We want initializedLater to present like a non-optional 'let' to user code;
// it can only be assigned once, and can't be accessed before being assigned.
var initializedLater: String {
get { return _initializedLater! }
set {
assert(_initializedLater == nil)
_initializedLater = newValue
}
}
}
Implicitly-unwrapped optionals allow this in a pinch, but give up a lot of safety compared to a non-optional 'let'. Using IUO for multi-phase initialization gives up both immutability and nil-safety.
The attribute @NSCopying
introduces a use of NSCopying.copy()
to
create a copy on assignment. The implementation pattern may look familiar:
class Foo {
// @NSCopying var text: NSAttributedString
var _text: NSAttributedString
var text: NSAttributedString {
get { return _text }
set { _text = newValue.copy() as! NSAttributedString }
}
}
We propose the introduction of property wrappers, which allow a property declaration to state which wrapper is used to implement it. The wrapper is described via an attribute:
@Lazy var foo = 1738
This implements the property foo
in a way described by the property wrapper type for Lazy
:
@propertyWrapper
enum Lazy<Value> {
case uninitialized(() -> Value)
case initialized(Value)
init(initialValue: @autoclosure @escaping () -> Value) {
self = .uninitialized(initialValue)
}
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)
}
}
}
A property wrapper type provides the storage for a property that
uses it as a wrapper. The wrappedValue
property of the wrapper type
provides the actual
implementation of the wrapper, while the (optional)
init(initialValue:)
enables initialization of the storage from a
value of the property's type. The property declaration
@Lazy var foo = 1738
translates to:
var $foo: Lazy<Int> = Lazy<Int>(initialValue: 1738)
var foo: Int {
get { return $foo.wrappedValue }
set { $foo.wrappedValue = newValue }
}
The use of the prefix $
for the synthesized storage property name is
deliberate: it provides a predictable name for the backing storage,
so that wrapper types can provide API. For example, we could provide
a reset(_:)
operation on Lazy
to set it back to a new value:
extension Lazy {
/// Reset the state back to "uninitialized" with a new,
/// possibly-different initial value to be computed on the next access.
mutating func reset(_ newValue: @autoclosure @escaping () -> Value) {
self = .uninitialized(newValue)
}
}
$foo.reset(42)
The property wrapper instance can be initialized directly by providing the initializer arguments in parentheses after the name. This could be used, for example, when a particular property wrapper requires more setup to provide access to a value (example courtesy of Harlan Haskins):
@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
}
Property wrappers can be applied to properties at global, local, or type scope. Those properties can have observing accessors (willSet
/didSet
), but not explicitly-written getters or setters.
Before describing the detailed design, here are some more examples of wrappers.
A property wrapper can model "delayed" initialization, where
the definite initialization (DI) rules for properties are enforced
dynamically rather than at compile time. This can avoid the need for
implicitly-unwrapped optionals in multi-phase initialization. We can
implement both a mutable variant, which allows for reassignment like a
var
:
@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
}
}
and an immutable variant, which only allows a single initialization like
a let
:
@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
}
}
}
This enables multi-phase initialization, like this:
class Foo {
@DelayedImmutable var x: Int
init() {
// We don't know "x" yet, and we don't have to set it
}
func initializeX(x: Int) {
self.x = x // Will crash if 'self.x' is already initialized
}
func getX() -> Int {
return x // Will crash if 'self.x' wasn't initialized
}
}
Many Cocoa classes implement value-like objects that require explicit copying.
Swift currently provides an @NSCopying
attribute for properties to give
them behavior like Objective-C's @property(copy)
, invoking the copy
method
on new objects when the property is set. We can turn this into a wrapper:
@propertyWrapper
struct Copying<Value: NSCopying> {
private var _value: Value
init(initialValue 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
}
}
}
This implementation would address the problem detailed in
SE-0153. Leaving the copy()
out of init(initialValue:)
implements the pre-SE-0153 semantics.
Support for atomic operations (load, store, increment/decementer, compare-and-exchange) is a commonly-requested Swift feature. While the implementation details for such a feature would involve compiler and standard library magic, the interface itself can be nicely expressed as a property wrapper type:
@propertyWrapper
struct Atomic<Value> {
private var _value: Value
init(initialValue: Value) {
self._value = initialValue
}
var wrappedValue: Value {
get { return load() }
set { store(newValue: newValue) }
}
func load(order: MemoryOrder = .relaxed) { ... }
mutating func store(newValue: Value, order: MemoryOrder = .relaxed) { ... }
mutating func increment() { ... }
mutating func decrement() { ... }
}
extension Atomic where Value: Equatable {
mutating func compareAndExchange(oldValue: Value, newValue: Value, order: MemoryOrder = .relaxed) -> Bool {
...
}
}
enum MemoryOrder {
case relaxed, consume, acquire, release, acquireRelease, sequentiallyConsistent
};
Here are some simple uses of Atomic
. With atomic types, it's fairly common
to weave lower-level atomic operations (increment
, load
, compareAndExchange
) where we need specific semantics (such as memory ordering) with simple queries, so both the property and the synthesized storage property are used often:
@Atomic var counter: Int
if thingHappened {
$counter.increment()
}
print(counter)
@Atomic var initializedOnce: Int?
if initializedOnce == nil {
let newValue: Int = /*computeNewValue*/
if !$initializedOnce.compareAndExchange(oldValue: nil, newValue: newValue) {
// okay, someone else initialized it. clean up if needed
}
}
print(initializedOnce)
Thread-specific storage (based on pthreads) can be implemented as a property wrapper, too (example courtesy of Daniel Delwood):
@propertyWrapper
final class ThreadSpecific<T> {
private var key = pthread_key_t()
private let initialValue: T
init(key: pthread_key_t, initialValue: T) {
self.key = key
self.initialValue = initialValue
}
init(initialValue: T) {
self.initialValue = initialValue
pthread_key_create(&key) {
// 'Any' erasure due to inability to capture 'self' or <T>
$0.assumingMemoryBound(to: Any.self).deinitialize(count: 1)
$0.deallocate()
}
}
deinit {
fatalError("\(ThreadSpecific<T>.self).deinit is unsafe and would leak")
}
private var box: UnsafeMutablePointer<Any> {
if let pointer = pthread_getspecific(key) {
return pointer.assumingMemoryBound(to: Any.self)
} else {
let pointer = UnsafeMutablePointer<Any>.allocate(capacity: 1)
pthread_setspecific(key, UnsafeRawPointer(pointer))
pointer.initialize(to: initialValue as Any)
return pointer
}
}
var wrappedValue: T {
get { return box.pointee as! T }
set (v) {
box.withMemoryRebound(to: T.self, capacity: 1) { $0.pointee = v }
}
}
}
With some work, property wrappers can provide copy-on-write wrappers (original example courtesy of Brent Royal-Gordon):
protocol Copyable: AnyObject {
func copy() -> Self
}
@propertyWrapper
struct CopyOnWrite<Value: Copyable> {
init(initialValue: Value) {
wrappedValue = initialValue
}
private(set) var wrappedValue: Value
var wrapperValue: Value {
mutating get {
if !isKnownUniquelyReferenced(&wrappedValue) {
wrappedValue = value.copy()
}
return wrappedValue
}
set {
wrappedValue = newValue
}
}
}
wrapperValue
provides delegation for the synthesized storage property, allowing the copy-on-write wrapper to be used directly:
@CopyOnWrite var storage: MyStorageBuffer
// Non-modifying access:
let index = storage.index(of: …)
// For modification, access $storage, which goes through `wrapperValue`:
$storage.append(…)
We can define a property wrapper type Ref
that is an abstracted reference
to some value that can be get/set, which is effectively a programmatic computed
property:
@propertyWrapper
struct Ref<Value> {
let read: () -> Value
let write: (Value) -> Void
var wrappedValue: Value {
get { return read() }
nonmutating set { write(newValue) }
}
subscript<U>(dynamicMember keyPath: WritableKeyPath<Value, U>) -> Ref<U> {
return Ref<U>(
read: { self.wrappedValue[keyPath: keyPath] },
write: { self.wrappedValue[keyPath: keyPath] = $0 })
}
}
The subscript is using SE-0252 "Key Path Member Lookup" so that a Ref
instance provides access to the properties of its value. Building on the example from SE-0252:
@Ref(read: ..., write: ...)
var rect: Rectangle
print(rect) // accesses the Rectangle
print(rect.topLeft) // accesses the topLeft component of the rectangle
let rect2 = $rect // get the Ref<Rectangle>
let topLeft2 = $rect.topLeft // get a Ref<Point> referring to the Rectangle's topLeft
The Ref
type encapsulates read/write, and making it a property wrapper lets
us primarily see the underlying value. Often, one does not want to explicitly
write out the getters and setters, and it's fairly common to have a Box
type that boxes up a value and can vend Ref
instances referring into that box. We can do so with another property wrapper:
@propertyWrapper
class Box<Value> {
var wrappedValue: Value
init(initialValue: Value) {
self.wrappedValue = initialValue
}
var wrapperValue: Ref<Value> {
return Ref<Value>(read: { self.wrappedValue }, write: { self.wrappedValue = $0 })
}
}
Now, we can define a new Box
directly:
@Box var rectangle: Rectangle = ...
print(rectangle) // access the rectangle
print(rectangle.topLeft) // access the top left coordinate of the rectangle
let rect2 = $rectangle // through wrapperValue, produces a Ref<Rectangle>
let topLeft2 = $rectangle.topLeft // through wrapperValue, produces a Ref<Point>
The use of wrapperValue
hides the box from the client (the storage variable is renamed to $$rectangle
and remains private), providing direct access to the value in the box (the common case) as well as access to the box contents via Ref (referenced as $rectangle
).
A property wrapper could limit the stored value to be within particular bounds. For example, the Clamping
property wrapper provides min/max bounds within which values will be clamped:
@propertyWrapper
struct Clamping<V: Comparable> {
var value: V
let min: V
let max: V
init(initialValue: V, min: V, max: V) {
value = initialValue
self.min = min
self.max = max
assert(value >= min && value <= max)
}
var wrappedValue: V {
get { return value }
set {
if newValue < min {
value = min
} else if newValue > max {
value = max
} else {
value = newValue
}
}
}
}
Most interesting in this example is how @Clamping
properties can be
initialized given both an initial value and initializer arguments. In such cases, the initialValue:
argument is placed first. For example, this means we can define a Color
type that clamps all values in the range [0, 255]:
struct Color {
@Clamping(min: 0, max: 255) var red: Int = 127
@Clamping(min: 0, max: 255) var green: Int = 127
@Clamping(min: 0, max: 255) var blue: Int = 127
@Clamping(min: 0, max: 255) var alpha: Int = 255
}
The synthesized memberwise initializer demonstrates how the initialization itself is formed:
init(red: Int = 127, green: Int = 127, blue: Int = 127, alpha: Int = 255) {
$red = Clamping(initialValue: red, min: 0, max: 255)
$green = Clamping(initialValue: green, min: 0, max: 255)
$blue = Clamping(initialValue: blue, min: 0, max: 255)
$alpha = Clamping(initialValue: alpha, min: 0, max: 255)
}
(Example courtesy of Avi)
There are a number of existing types that already provide the basic structure of a property wrapper type. One fun case is Unsafe(Mutable)Pointer
, which we could augment to allow easy access to the pointed-to value:
@propertyWrapper
struct UnsafeMutablePointer<Pointee> {
var pointee: Pointee { ... }
var wrappedValue: Pointee {
get { return pointee }
set { pointee = newValue }
}
}
From a user perspective, this allows us to set up the unsafe mutable pointer's address once, then mostly refer to the pointed-to value:
@UnsafeMutablePointer(mutating: addressOfAnInt)
var someInt: Int
someInt = 17 // equivalent to someInt.pointee = 17
print(someInt)
$someInt.deallocate()
RxCocoa's BehaviorRelay
replays the most recent value provided to it for each of the subscribed observers. It is created with an initial value, has wrappedValue
property to access the current value, as well as API to subscribe
a new observer: (Thanks to Adrian Zubarev for pointing this out)
@BehaviorRelay
var myValue: Int = 17
let observer = $myValue.subscribe(...) // subscribe an observer
$myValue.accept(42) // set a new value via the synthesized storage property
print(myValue) // print the most recent value
When multiple property wrappers are provided for a given property,
the wrappers are composed together to get both effects. For example, consider the composition of DelayedMutable
and Copying
:
@DelayedMutable @Copying var path: UIBezierPath
Here, we have a property for which we can delay initialization until later. When we do set a value, it will be copied via NSCopying
's copy
method.
Composition is implemented by nesting later wrapper types inside earlier wrapper types, where the innermost nested type is the original property's type. For the example above, the backing storage will be of type DelayedMutable<Copying<UIBezierPath>>
, and the synthesized getter/setter for path
will look through both levels of .wrappedValue
:
var $path: DelayedMutable<Copying<UIBezierPath>> = .init()
var path: UIBezierPath {
get { return $path.wrappedValue.wrappedValue }
set { $path.wrappedValue.wrappedValue = newValue }
}
Note that this design means that property wrapper composition is not commutative, because the order of the attributes affects how the nesting is performed:
@DelayedMutable @Copying var path1: UIBezierPath // $path1 has type DelayedMutable<Copying<UIBezierPath>>
@Copying @DelayedMutable var path2: UIBezierPath // error: $path2 has ill-formed type Copying<DelayedMutable<UIBezierPath>>
In this case, the type checker prevents the second ordering, because DelayedMutable
does not conform to the NSCopying
protocol. This won't always be the case: some semantically-bad compositions won't necessarily by caught by the type system. Alternatives to this approach to composition are presented in "Alternatives considered."
A property wrapper type is a type that can be used as a property wrapper. There are two basic requirements for a property wrapper type:
- The property wrapper type must be defined with the attribute
@propertyWrapper
. The attribute indicates that the type is meant to be used as a property wrapper type, and provides a point at which the compiler can verify any other consistency rules. - The property wrapper type must have a property named
wrappedValue
, whose access level is the same as that of the type itself. This is the property used by the compiler to access the underlying value on the wrapper instance.
Introducing a property wrapper to a property makes that property computed (with a getter/setter) and introduces a stored property whose type is the wrapper type. That stored property can be initialized in one of three ways:
-
Via a value of the original property's type (e.g.,
Int
in@Lazy var foo: Int
, using the the property wrapper type'sinit(initialValue:)
initializer. That initializer must have a single parameter of the same type as thewrappedValue
property (or be an@autoclosure
thereof) and have the same access level as the property wrapper type itself. Wheninit(initialValue:)
is present, is is always used for the initial value provided on the property declaration. For example:@Lazy var foo = 17 // ... implemented as var $foo: Lazy = Lazy(initialValue: 17) var foo: Int { /* access via $foo.wrappedValue as described above */ }
When there are multiple, composed property wrappers, all of them must provide an init(initialValue:)
, and the resulting initialization will wrap each level of call:
@Lazy @Copying var path = UIBezierPath()
// ... implemented as
var $path: Lazy<Copying<UIBezierPath>> = .init(initialValue: .init(initialValue: UIBezierPath()))
var path: UIBezierPath { /* access via $path.wrappedValue.wrappedValue as described above */ }
-
Via a value of the property wrapper type, by placing the initializer arguments after the property wrapper type:
var addressOfInt: UnsafePointer<Int> = ... @UnsafeMutablePointer(mutating: addressOfInt) var someInt: Int // ... implemented as var $someInt: UnsafeMutablePointer<Int> = UnsafeMutablePointer(mutating: addressOfInt) var someInt: Int { /* access via $someInt.wrappedValue */ }
When there are multiple, composed property wrappers, only the first (outermost) wrapper may have initializer arguments.
-
Implicitly, when no initializer is provided and the property wrapper type provides a no-parameter initializer (
init()
). In such cases, the wrapper type'sinit()
will be invoked to initialize the stored property.@DelayedMutable var x: Int // ... implemented as var $x: DelayedMutable<Int> = DelayedMutable<Int>() var x: Int { /* access via $x.wrappedValue */ }
When there are multiple, composed property wrappers, only the first (outermost) wrapper needs to have an init()
.
If the first property wrapper type is generic, its generic arguments must either be given explicitly in the attribute or Swift must be able to deduce them from the variable declaration. That deduction proceeds as follows:
-
If the variable has an initial value expression
E
, then the first wrapper type is constrained to equal the type resulting from a call toA(initialValue: E, argsA...)
, whereA
is the written type of the attribute andargsA
are the arguments provided to that attribute. For example:@Lazy var foo = 17 // type inference as in... var $foo: Lazy = Lazy(initialValue: 17) // infers the type of '$foo' to be 'Lazy<Int>'
If there are multiple wrapper attributes, the argument to this call will instead be a nested call to
B(initialValue: E, argsB...)
for the written type of the next attribute, and so on recursively. For example:@A @B(name: "Hello") var bar = 42 // type inference as in ... var $bar = A(initialValue: B(initialValue: 42, name: "Hello")) // infers the type of '$bar' to be 'A<B<Int>'
-
Otherwise, if the first wrapper attribute has direct initialization arguments
E...
, the outermost wrapper type is constrained to equal the type resulting fromA(E...)
, whereA
is the written type of the first attribute. Wrapper attributes after the first may not have direct initializers. For example:@UnsafeMutablePointer(mutating: addressOfInt) var someInt // type inference as in... var $someInt: UnsafeMutablePointer = UnsafeMutablePointer.init(mutating: addressOfInt) // infers the type of `$someInt` to be `UnsafeMutablePointer<Int>`
-
Otherwise, if there is no initialization, and the original property has a type annotation, the type of the
wrappedValue
property in the last wrapper type is constrained to equal the type annotation of the original property. For example:@propertyWrapper struct Function<T, U> { var wrappedValue: (T) -> U? { ... } } @Function var f: (Int) -> Float? // infers T=Int, U=Float
In any case, the first wrapper type is constrained to be a specialization of the first attribute's written type. Furthermore, for any secondary wrapper attributes, the type of the wrappedValue property of the previous wrapper type is constrained to be a specialization of the attribute's written type. Finally, if a type annotation is given, the type of the wrappedValue property of the last wrapper type is constrained to equal the type annotation. If these rules fail to deduce all the type arguments for the first wrapper type, or if they are inconsistent with each other, the variable is ill-formed. For example:
@Lazy<Int> var foo: Int // okay
@Lazy<Int> var bar: Double // error: Lazy<Int>.wrappedValue is of type Int, not Double
The deduction can also provide a type for the original property (if a type annotation was omitted) or deduce generic arguments that have omitted from the type annotation. For example:
@propertyWrapper
struct StringDictionary {
var wrappedValue: [String: String]
}
@StringDictionary var d1. // infers Dictionary<String, String>
@StringDictionary var d2: Dictionary // infers <String, String>
Property wrappers are a form of custom attribute, where the attribute syntax is used to refer to entities declared in Swift. Grammatically, the use of property wrappers is described as follows:
attribute ::= '@' type-identifier expr-paren?
The type-identifier must refer to a property wrapper type, which can include generic arguments. Note that this allows for qualification of the attribute names, e.g.,
@Swift.Lazy var foo = 1742
The expr-paren, if present, provides the initialization arguments for the wrapper instance.
This formulation of custom attributes fits in with a larger proposal for custom attributes, which uses the same custom attribute syntax as the above but allows for other ways in which one can define a type to be used as an attribute. In this scheme, @propertyWrapper
is just one kind of custom attribute: there will be other kinds of custom attributes that are available only at compile time (e.g., for tools) or runtime (via some reflection capability).
Generally, a property that has a property wrapper will have both a getter and a setter. However, the setter may be missing if the wrappedValue
property of the property wrapper type lacks a setter, or its setter is inaccessible.
The synthesized getter will be mutating
if the property wrapper type's wrappedValue
property is mutating
and the property is part of a struct
. Similarly, the synthesized setter will be nonmutating
if either the property wrapper type's wrappedValue
property has a nonmutating
setter or the property wrapper type is a class
. For example:
@propertyWrapper
struct MutatingGetterWrapper<Value> {
var wrappedValue: Value {
mutating get { ... }
set { ... }
}
}
@propertyWrapper
struct NonmutatingSetterWrapper<Value> {
var wrappedValue: Value {
get { ... }
nonmutating set { ... }
}
}
@propertyWrapper
class ReferenceWrapper<Value> {
var wrappedValue: Value
}
struct Usewrappers {
// x's getter is mutating
// x's setter is mutating
@MutatingGetterWrapper var x: Int
// y's getter is nonmutating
// y's setter is nonmutating
@NonmutatingSetterWrapper var y: Int
// z's getter is nonmutating
// z's setter is nonmutating
@ReferenceWrapper var z: Int
}
A property that has a wrapper can be initialized after it is defined,
either via the property itself (if the wrapper type has an
init(initialValue:)
) or via the synthesized storage property. For
example:
@Lazy var x: Int
// ...
x = 17 // okay, treated as $x = .init(initialValue: 17)
The synthesized storage property can also be initialized directly, e.g.,
@UnsafeMutable var y: Int
// ...
$y = UnsafeMutable<Int>(pointer: addressOfInt) // okay
Note that the rules of definite
initialization (DI)
apply to properties that have wrappers. Let's expand the example of
x
above to include a re-assignment and use var
:
@Lazy var x2: Int
// ...
x2 = 17 // okay, treated as $x2 = .init(initialValue: 17)
// ...
x2 = 42 // okay, treated as x2 = 42 (calls the Lazy.wrappedValue setter)
Structs implicitly declare memberwise initializers based on the stored properties of the struct. With a property that has a wrapper, the property is technically computed because it's the synthesized property (of the wrapper's type) that is stored. Instance properties that have a property wrapper will have a corresponding parameter in the memberwise initializer, whose type will either be the original property type or the wrapper type, depending on the wrapper type and the initial value (if provided). Specifically, the memberwise initializer parameter for an instance property with a property wrapper will have the original property type if either of the following is true:
- The corresponding property has an initial value specified with the
=
syntax, e.g.,@Lazy var i = 17
, or
- The corresponding property has no initial value, but the property
wrapper type has an
init(initialValue:)
.
Otherwise, the memberwise initializer parameter will have the same type as the wrapper. For example:
struct Foo {
@UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
var x: Bool
@Lazy var y: Int = 17
@Lazy(closure: { getBool() }) var z: Bool
@CopyOnWrite var w: Image
// implicit memberwise initializer:
init(x: UserDefault<Bool> = UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false),
y: Int = 17,
z: Lazy<Bool> = Lazy(closure: { getBool() }),
w: Image) {
self.$x = x
self.$y = Lazy(initialValue: y)
self.$z = z
self.$w = CopyOnWrite(initialValue: w)
}
}
Synthesis for Encodable
, Decodable
, Hashable
, and Equatable
use the backing storage property. This allows property wrapper types to determine their own serialization and equality behavior. For Encodable
and Decodable
, the name used for keyed archiving is that of the original property declaration (without the $
).
Currently, identifiers starting with a $
are not permitted in Swift programs. Today, such identifiers are only used in LLDB, where they can be used to name persistent values within a debugging session.
This proposal loosens these rules slightly: the Swift compiler will introduce identifiers that start with $
(for the synthesized storage property), and Swift code can reference those properties. However, Swift code cannot declare any new entities with an identifier that begins with $
. For example:
@Lazy var x = 17
print($x) // okay to refer to compiler-defined $x
let $y = 17 // error: cannot declare entity with $-prefixed name '$y'
A property wrapper type can choose to hide its instance entirely by providing a property named wrapperValue
. As with the wrappedValue
property andinit(initialValue:)
, the wrapperValue
property must have the
same access level as its property wrapper type. When present, the synthesized storage property is hidden further (the private variable is named, e.g., $$foo
) and the property $foo
becomes a computed property accessing the storage property's wrapperValue
. For example:
class StorageManager {
func allocate<T>(_: T.Type) -> UnsafeMutablePointer<T> { ... }
}
@propertyWrapper
struct LongTermStorage<Value> {
let pointer: UnsafeMutablePointer<Value>
init(manager: StorageManager, initialValue: Value) {
pointer = manager.allocate(Value.self)
pointer.initialize(to: initialValue)
}
var wrappedValue: Value {
get { return pointer.pointee }
set { pointer.pointee = newValue }
}
var wrapperValue: UnsafeMutablePointer<Value> {
return pointer
}
}
When we use the LongTermStorage
wrapper, it handles the coordination with the StorageManager
and provides either direct access or an UnsafeMutablePointer
with which to manipulate the value:
let manager = StorageManager(...)
@LongTermStorage(manager: manager, initialValue: "Hello")
var someValue: String
print(someValue) // prints "Hello"
someValue = "World" // update the value in storage to "World"
// $someValue accesses the wrapperValue property of the wrapper instance, which
// is an UnsafeMutablePointer<String>
let world = $someValue.move() // take value directly from the storage
$someValue.initialize(to: "New value")
The $
variable's access level will be the more restrictive of either internal
or the access level of the original property. For example:
@LongTermStorage(manager: manager, initialValue: "Hello")
public var someValue: String
is translated into:
private var $$someValue: LongTermStorage<String> = LongTermStorage(manager: manager, initialValue: "Hello")
internal var $someValue: UnsafeMutablePointer<String> {
get { return $$someValue.wrapperValue }
set { $$someValue.wrapperValue = newValue }
}
public var someValue: String {
get { return $$someValue.wrappedValue }
set { $$someValue.wrappedValue = newValue }
}
There could then be other kinds of storage (e.g., some arena-based storage) described as property wrappers that also vend their wrapper types as UnsafeMutablePointer
:
@propertyWrapper
struct ArenaStorage<Value> {
let pointer: UnsafeMutablePointer<Value>
init(arena: StorageArena, initialValue: Value) {
pointer = arena.allocate(Value.self)
pointer.initialize(to: initialValue)
}
var wrappedValue: Value {
get { return pointer.pointee }
set { pointer.pointee = newValue }
}
var wrapperValue: UnsafeMutablePointer<Value> {
return pointer
}
}
The someValue
variable from the previous example could be switched over to use arena-based storage without changing any of the clients of someValue
or its wrapper property $someValue
:
@ArenaStorage(arena: currentConnectionArena, initialValue: "Hello")
var someValue: String
Each of the property wrapper types could have different implementations with
different data, but all of them present the same interface through $someValue
and someValue
. Note also that the $someValue
is not writable, because wrapperValue
is a get-only property.
There are a number of restrictions on the use of property wrappers when defining a property:
- A property with a wrapper may not be declared inside a protocol.
- An instance property with a wrapper may not be declared inside an extension.
- An instance property may not be declared in an
enum
. - A property with a wrapper that is declared within a class cannot override another property.
- A property with a wrapper cannot be
lazy
,@NSCopying
,@NSManaged
,weak
, orunowned
. - A property with a wrapper must be the only property declared within its enclosing declaration (e.g.,
@Lazy var (x, y) = /* ... */
is ill-formed). - A property with a wrapper shall not define a getter or setter.
- The
wrappedValue
property and (if present)init(initialValue:)
of a property wrapper type shall have the same access as the property wrapper type. - The
wrapperValue
property, if present, shall have the same access as the property wrapper type. - The
init()
initializer, if present, shall have the same access as the property wrapper type.
By itself, this is an additive feature that doesn't impact existing
code. However, with some of the property wrappers suggested, it can
potentially obsolete existing, hardcoded language
features. @NSCopying
could be completely replaced by a Copying
property wrapper type introduced in the Foundation
module. lazy
cannot be completely replaced because it's initial value can refer to
the self
of the enclosing type; see 'deferred evaluation of
initialization expressions_. However, it may still make sense to
introduce a Lazy
property wrapper type to cover many of the common
use cases, leaving the more-magical lazy
as a backward-compatibility
feature.
The property wrappers language feature as proposed has no impact on the ABI or runtime. Binaries that use property wrappers can be backward-deployed to the Swift 5.0 runtime.
Composition was left out of the first revision of this proposal, because one can manually compose property wrapper types. For example, the composition @A @B
could be implemented as an AB
wrapper:
@propertyWrapper
struct AB<Value> {
private var storage: A<B<Value>>
var wrappedValue: Value {
get { storage.wrappedValue.wrappedValue }
set { storage.wrappedValue.wrappedValue = newValue }
}
}
The main benefit of this approach is its predictability: the author of AB
decides how to best achieve the composition of A
and B
, names it appropriately, and provides the right API and documentation of its semantics. On the other hand, having to manually write out each of the compositions is a lot of boilerplate, particularly for a feature whose main selling point is the elimination of boilerplate. It is also unfortunate to have to invent names for each composition---when I try the compose A
and B
via @A @B
, how do I know to go look for the manually-composed property wrapper type AB
? Or maybe that should be BA
?
One proposed approach to composition addresses only the last issue above directly, treating the attribute-composition syntax @A @B
as a lookup of the nested type B
inside A
to find the wrapper type:
@propertyWrapper
struct A<Value> {
var wrappedValue: Value { ... }
}
extension A {
typealias B = AB<Value>
}
This allows the natural composition syntax @A @B
to work, redirecting to manually-written property wrappers that implement the proper semantics and API. Additionally, this scheme allows one to control which compositions are valid: if there is no nested type B
in A
, the composition is invalid. If both A.B
and B.A
exist, we have a choice: either enforce commutative semantics as part of the language (B.A
and A.B
must refer to the same type or the composition @A @B
is ill-formed), or allow them to differ (effectively matching the semantics of this proposal).
This approach addresses the syntax for composition while maintaining control over the precise semantics of composition via manually-written wrapper types. However, it does not address the boilerplate problem.
There has been a desire to effect composition of property wrappers without having to wrap one property wrapper type in the other. For example, to have @A @B
apply the policies of both A
and B
without producing a nested type like A<B<Int>>
. This would make potentially make composition more commutative, at least from the type system perspective. However, this approach does not fit with the "wrapper" approach taken by property wrappers. In a declaration
@A @B var x: Int
the Int
value is conceptually wrapped by a property wrapper type, and the property wrapper type's wrappedValue
property guards access to that (conceptual) Int
value. That Int
value cannot be wrapped both by instances of both A
and B
without either duplicating data (both A
and B
have a copy of the Int
) or nesting one of the wrappers inside the other. With the copying approach, one must maintain consistency between the copies (which is particularly hard when value types are involved) and there will still be non-commutative compositions. Nesting fits better with the "wrapper" model of property wrappers.
Instead of a new attribute, we could introduce a PropertyWrapper
protocol to describe the semantic constraints on property wrapper
types. It might look like this:
protocol PropertyWrapper {
associatedtype Value
var wrappedValue: Value { get }
}
There are a few issues here. First, a single protocol
PropertyWrapper
cannot handle all of the variants of wrappedValue
that
are implied by the section on mutability of properties with wrappers,
because we'd need to cope with mutating get
as well as set
and
nonmutating set
. Moreover, protocols don't support optional
requirements, like init(initialValue:)
(which also has two
forms: one accepting a Value
and one accepting an @autoclosure () -> Value
) and init()
. To cover all of these cases, we would need a
several related-but-subtly-different protocols.
The second issue that, even if there were a single PropertyWrapper
protocol, we don't know of any useful generic algorithms or data
structures that seem to be implemented in terms of only
PropertyWrapper
.
A previous iteration of this proposal (and its implementation) used by
syntax similar to that of Kotlin's wrapperd
properties, where the by
followed the variable declaration. For example:
var foo by Lazy = 1738
static var isFooFeatureEnabled: Bool by UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
There are some small advantages to this syntax over the attribute formulation:
- For cases like
UserDefault
where the wrapper instance is initialized directly, the initialization happens after the original variable declaration, which reads better because the variable type and name come first, and how it's implemented come later. (Counter point: Swift developers are already accustomed to reading past long attributes, which are typically placed on the previous line) - The
by wrapperType
formulation leaves syntactic space for add-on features like specifying the access level of the wrapper instance (by private wrapperType
) or delegating to an existing property (by someInstanceProperty
).
The main problem with by
is its novelty: there isn't anything else in Swift quite like the by
keyword above, and it is unlikely that the syntax would be re-used for any other feature. As a keyword, by
is quite meaningless, and brainstorming during the initial pitch didn't find any clearly good names for this functionality.
Property wrappers address a similar set of use cases to property behaviors, which were proposed and reviewed in late 2015/early 2016. The design did not converge, and the proposal was deferred. This proposal picks up the thread, using much of the same motivation and some design ideas, but attempting to simplify the feature and narrow the feature set. Some substantive differences from the prior proposal are:
- Behaviors were introduced into a property with the
[behavior]
syntax, rather than the attribute syntax described here. See the property behaviors proposal for more information. - Wrappers are always expressed by a (generic) type. Property behaviors
had a new kind of declaration (introduced by the
behavior
keyword). Having a new kind of declaration allowed for the introduction of specialized syntax, but it also greatly increased the surface area (and implementation cost) of the proposal. Using a generic type makes property wrappers more of a syntactic-sugar feature that is easier to implement and explain. - Wrappers cannot declare new kinds of accessors (e.g., the
didChange
example from the property behaviors proposal). - Wrappers used for properties declared within a type cannot refer to
the
self
of their enclosing type. This eliminates some use cases (e.g., implementing aSynchronized
property wrapper type that uses a lock defined on the enclosing type), but simplifies the design. - Wrappers can be initialized out-of-line, and one
can use the
$
-prefixed name to refer to the storage property. These were future directions in the property behaviors proposal.
By default, the synthesized storage property will have private
access. However, there are various circumstances where it would be beneficial to expose the synthesized storage property. This could be performed "per-property", e.g., by introducing a syntax akin to private(set)
:
// both foo and $foo are publicly visible
@Atomic
public public(wrapper) var foo: Int = 1738
One could also consider having the property wrapper types themselves declare that the synthesized storage properties for properties using those wrappers should have the same access as the original property. For example:
@propertyWrapper(wrapperIsAccessible: true)
struct Atomic<T> {
var wrappedValue: T { ... }
}
// both bar and $bar are publicly visible
@Atomic
public var bar: Int = 1738
The two features could also be combined, allowing property wrapper types to provide the default behavior and the access-level(wrapper)
syntax to change the default. The current proposal's private
-by-default is meant to be a conservative first step to allow a separate exploration into expanding the visibility of the backing storage.
Manually-written getters and setters for properties declared in a type often refer to the self
of their enclosing type. For example, this can be used to notify clients of a change to a property's value:
public class MyClass: Superclass {
private var backingMyVar: Int
public var myVar: Int {
get { return backingMyVar }
set {
if newValue != backingMyVar {
self.broadcastValueChanged(oldValue: backingMyVar, newValue: newValue)
}
backingMyVar = newValue
}
}
}
This "broadcast a notification that the value has changed" implementation cannot be cleanly factored into a property behavior type, because it needs access to both the underlying storage value (here, backingMyVar
) and the self
of the enclosing type. We could require a separate call to register the self
instance with the wrapper type, e.g.,
protocol Observed {
func broadcastValueChanged<T>(oldValue: T, newValue: T)
}
@propertyWrapper
public struct Observable<Value> {
public var stored: Value
var observed: Observed?
public init(initialValue: Value) {
self.stored = initialValue
}
public func register(_ observed: Observable) {
self.observed = observed
}
public var wrappedValue: Value {
get { return stored }
set {
if newValue != stored {
observed?.broadcastValueChanged(oldValue: stored, newValue: newValue)
}
stored = newValue
}
}
}
However, this means that one would have to manually call register(_:)
in the initializer for MyClass
:
public class MyClass: Superclass {
@Observable public var myVar: Int = 17
init() {
// self.$myVar gets initialized with Observable(initialValue: 17) here
super.init()
self.$myVar.register(self) // register as an Observable
}
}
This isn't as automatic as we would like, and it requires us to have a separate reference to the self
that is stored within Observable
.
Instead, we could extend the ad hoc protocol used to access the storage property of a @propertyWrapper
type a bit further. Instead of (or in addition to) a wrappedValue
property, a property wrapper type could provide a subscript(instanceSelf:)
and/or subscript(typeSelf:)
that receive self
as a parameter. For example:
@propertyWrapper
public struct Observable<Value> {
public var stored: Value
public init(initialValue: Value) {
self.stored = initialValue
}
public subscript<OuterSelf: Observed>(instanceSelf observed: OuterSelf) -> Value {
get { return stored }
set {
if newValue != stored {
observed.broadcastValueChanged(oldValue: stored, newValue: newValue)
}
stored = newValue
}
}
}
The (generic) subscript gets access to the enclosing self
type via its subscript parameter, eliminating the need for the separate register(_:)
step and the (type-erased) storage of the outer self
. The desugaring within MyClass
would be as follows:
public class MyClass: Superclass {
@Observable public var myVar: Int = 17
// desugars to...
private var $myVar: Observable<Int> = Observable(initialValue: 17)
public var myVar: Int {
get { return $myVar[instanceSelf: self] }
set { $myVar[instanceSelf: self] = newValue }
}
}
This change is backward-compatible with the rest of the proposal. Property wrapper types could provide any (non-empty) subset of the three ways to access the underlying value:
- For instance properties,
subscript(instanceSelf:)
as shown above. - For static or class properties,
subscript(typeSelf:)
, similar to the above but accepting a metatype parameter. - For global/local properties, or when the appropriate
subscript
mentioned above isn't provided by the wrapper type, thewrappedValue
property would be used.
The main challenge with this design is that it doesn't directly work when the enclosing type is a value type and the property is settable. In such cases, the parameter to the subscript would get a copy of the entire enclosing value, which would not allow mutation, On the other hand, one could try to pass self
as inout
, e.g.,
public struct MyStruct {
@Observable public var myVar: Int = 17
// desugars to...
private var $myVar: Observable<Int> = Observable(initialValue: 17)
public var myVar: Int {
get { return $myVar[instanceSelf: self] }
set { $myVar[instanceSelf: &self] = newValue }
}
}
There are a few issues here: first, subscripts don't allow inout
parameters in the first place, so we would have to figure out how to implement support for such a feature. Second, passing self
as inout
while performing access to the property self.myVar
violates Swift's exclusivity rules (generalized accessors might help address this). Third, property wrapper types that want to support subscript(instanceSelf:)
for both value and reference types would have to overload on inout
or would have to have a different subscript name (e.g., subscript(mutatingInstanceSelf:)
).
So, while we feel that support for accessing the enclosing type's self
is useful and as future direction, and this proposal could be extended to accommodate it, the open design questions are significant enough that we do not want to tackle them all in a single proposal.
When specifying a wrapper for a property, the synthesized storage property is implicitly created. However, it is possible that there already exists a property that can provide the storage. One could provide a form of property delegation that creates the getter/setter to forward to an existing property, e.g.:
lazy var fooBacking: SomeWrapper<Int>
@wrapper(to: fooBacking) var foo: Int
One could express this either by naming the property directly (as above) or, for an even more general solution, by providing a keypath such as \.someProperty.someOtherProperty
.
- The name of the feature has been changed from "property delegates" to "property wrappers" to better communicate how they work and avoid the existing uses of the term "delegate" in the Apple developer community
- When a property wrapper type has a no-parameter
init()
, properties that use that wrapper type will be implicitly initialized viainit()
. - Support for property wrapper composition has been added, using a "nesting" model.
- A property with a wrapper can no longer have an explicit
get
orset
declared, to match with the behavior of existing, similar features (lazy
,@NSCopying
). - A property with a wrapper does not need to be
final
. - Reduced the visibility of the synthesized storage property to
private
. - When a wrapper type provides
wrapperValue
, the (computed)$
variable isinternal
(at most) and the backing storage variable gets the prefix$$
(and remains private). - Removed the restriction banning property wrappers from having names that match the regular expression
_*[a-z].*
. Codable
,Hashable
, andEquatable
synthesis are now based on the backing storage properties, which is a simpler model that gives more control to the authors of property wrapper types.- Improved type inference for property wrapper types and clarified that the type of the
wrappedValue
property is used as part of this inference. See the "Type inference" section. - Renamed the
value
property towrappedValue
to avoid conflicts. - Initial values and explicitly-specified initializer arguments can both be used together; see the
@Clamping
example.
This proposal was greatly improved throughout its first pitch by many people. Harlan Haskins, Brent Royal-Gordon, Adrian Zubarev, Jordan Rose and others provided great examples of uses of property wrappers (several of which are in this proposal). Adrian Zubarev and Kenny Leung helped push on some of the core assumptions and restrictions of the original proposal, helping to make it more general. Vini Vendramini and David Hart helped tie this proposal together with custom attributes, which drastically reduced the syntactic surface area of this proposal.