Last active
June 24, 2020 12:40
-
-
Save MMnasrabadi/b9b8d2a69eb694a44feb28597f8374b2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Property Wrappers | |
https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Property Wrappers
Contents
NSCopying
Atomic
Ref
/Box
@propertyWrapper
by
syntax$
projection propertyIntroduction
There are property implementation patterns that come up repeatedly.
Rather than hardcode a fixed set of patterns into the compiler (as we have done for
lazy
and@NSCopying
),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.
Pitch #1
Pitch #2
Pitch #3
Motivation
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 provideslazy
properties as a primitive language feature, since lazy initialization is common and is often necessary to avoid having properties be exposed asOptional
. Without this language support, it takes a lot of boilerplate to get the same effect:Building
lazy
into the language has several disadvantages. It makes thelanguage 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:
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 ofNSCopying.copy()
tocreate a copy on assignment. The implementation pattern may look familiar:
Proposed solution
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:
This implements the property
foo
in a way described by the property wrapper type forLazy
:A property wrapper type provides the storage for a property that
uses it as a wrapper. The
wrappedValue
property of the wrapper typeprovides the actual
implementation of the wrapper, while the (optional)
init(wrappedValue:)
enables initialization of the storage from avalue of the property's type. The property declaration
translates to:
The use of the prefix
_
for the synthesized storage property name isdeliberate: it provides a predictable name for the synthesized storage property that
fits established conventions for
private
stored properties. For example,we could provide a
reset(_:)
operation onLazy
to set it back to a newvalue:
The backing storage property can also be explicitly initialized. For example:
The property wrapper instance can be initialized directly by providing the initializer arguments in parentheses after the name. The above code can be written equivalently in a single declaration as:
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.The
Lazy
property wrapper has little or no interesting API outside of its initializers, so it is not important to export it to clients. However, property wrappers can also describe rich relationships that themselves have interesting API. For example, we might have a notion of a property wrapper that references a database field established by name (example inspired by Tanner):We could define our model based on the
Field
property wrapper:Field
itself has API that is important to users ofPerson
: it lets us flush existing values, fetch new values, and retrieve the name of the corresponding field in the database. However, the underscored variables for each of the properties of our model (_firstName
,_lastName
, and_birthdate
) areprivate
, so our clients cannot manipulate them directly.To vend API, the property wrapper type
Field
can provide a projection that allows us to manipulate the relationship of the field to the database. Projection properties are prefixed with a$
, so the projection of thefirstName
property is called$firstName
and is visible whereverfirstName
is visible. Property wrapper types opt into provided a projection by defining aprojectedValue
property:When
projectedValue
is present, the projection variable is created as a wrapper aroundprojectedValue
. For example, the following property:expands to:
This allows clients to manipulate both the property and its projection, e.g.,
Examples
Before describing the detailed design, here are some more examples of
wrappers.
Delayed Initialization
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
:and an immutable variant, which only allows a single initialization like
a
let
:This enables multi-phase initialization, like this:
NSCopying
Many Cocoa classes implement value-like objects that require explicit copying.
Swift currently provides an
@NSCopying
attribute for properties to givethem behavior like Objective-C's
@property(copy)
, invoking thecopy
methodon new objects when the property is set. We can turn this into a wrapper:
This implementation would address the problem detailed in
SE-0153. Leaving the
copy()
out ofinit(wrappedValue:)
implements the pre-SE-0153 semantics.Atomic
Support for atomic operations (load, store, increment/decrement, 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:
Here are some simple uses of
Atomic
. With atomic types, it's fairly commonto 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:Thread-specific storage
Thread-specific storage (based on pthreads) can be implemented as a property wrapper, too (example courtesy of Daniel Delwood):
User defaults
Property wrappers can be used to provide typed properties into for
string-keyed data, such as user defaults (example courtesy of Harlan Haskins),
encapsulating the mechanism for extracting that data in the wrapper type.
For example:
Copy-on-write
With some work, property wrappers can provide copy-on-write wrappers (original example courtesy of Brent Royal-Gordon):
projectedValue
provides projection for the synthesized storage property, allowing the copy-on-write wrapper to be used directly:Ref
/Box
We can define a property wrapper type
Ref
that is an abstracted referenceto some value that can be get/set, which is effectively a programmatic computed property:
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:The
Ref
type encapsulates read/write, and making it a property wrapper letsus 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 vendRef
instances referring into that box. We can do so with another property wrapper:Now, we can define a new
Box
directly:The use of
projectedValue
hides the box from the client (_rectangle
remains private), providing direct access to the value in the box (the common case) as well as access to the box contents viaRef
(referenced as$rectangle
)."Clamping" a value within bounds
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:Most interesting in this example is how
@Clamping
properties can beinitialized given both an initial value and initializer arguments. In such cases, the
wrappedValue:
argument is placed first. For example, this means we can define aColor
type that clamps all values in the range [0, 255]:The synthesized memberwise initializer demonstrates how the initialization itself is formed:
(Example courtesy of Avi)
Property wrapper types in the wild
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: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:
RxCocoa's
BehaviorRelay
replays the most recent value provided to it for each of the subscribed observers. It is created with an initial value, haswrappedValue
property to access the current value and aprojectedValue
to expose a projection providing API tosubscribe
a new observer: (Thanks to Adrian Zubarev for pointing this out)Combine's
Published
property wrapper is similar in spirit, allowing clients to subscribe to@Published
properties (via the$
projection) to receive updates when the value changes.SwiftUI makes extensive use of
property wrappers to declare local state (
@State
) and express data dependencies on other state that can effect the UI (@EnvironmentObject
,@Environment
,@ObjectBinding
). It makes extensive use of projections to theBinding
property wrapper to allow controlled mutation of the state that affects UI.Composition of property wrappers
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
andCopying
: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
'scopy
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 forpath
will look through both levels of.wrappedValue
:Note that this design means that property wrapper composition is not commutative, because the order of the attributes affects how the nesting is performed:
In this case, the type checker prevents the second ordering, because
DelayedMutable
does not conform to theNSCopying
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."Detailed design
Property wrapper types
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:
@propertyWrapper
. The attribute indicates that the type is meant tobe used as a property wrapper type, and provides a point at which the
compiler can verify any other consistency rules.
wrappedValue
, whoseaccess 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.
Initialization of synthesized storage properties
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(wrappedValue:)
initializer. That initializer must have a singleparameter of the same type as the
wrappedValue
property (orbe an
@autoclosure
thereof) and have the same access level as theproperty wrapper type itself. When
init(wrappedValue:)
is present,is is always used for the initial value provided on the property
declaration. For example:
When there are multiple, composed property wrappers, all of them must provide an
init(wrappedValue:)
, and the resulting initialization will wrap each level of call:Via a value of the property wrapper type, by placing the initializer
arguments after the property wrapper type:
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.When there are multiple, composed property wrappers, only the first (outermost) wrapper needs to have an
init()
.Type inference with property wrappers
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(wrappedValue: E, argsA...)
, whereA
is the written type of the attribute andargsA
are the arguments provided to that attribute. For example:If there are multiple wrapper attributes, the argument to this call will instead be a nested call to
B(wrappedValue: E, argsB...)
for the written type of the next attribute, and so on recursively. For example: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: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: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:
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:
Custom attributes
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:
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.,
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).Mutability of properties with wrappers
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'swrappedValue
property ismutating
and the property is part of astruct
. Similarly, the synthesized setter will benonmutating
if either the property wrapper type'swrappedValue
property has anonmutating
setter or the property wrapper type is aclass
. For example:Out-of-line initialization of properties with wrappers
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(wrappedValue:)
) or via the synthesized storage property. Forexample:
The synthesized storage property can also be initialized directly,
e.g.,
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 usevar
:Memberwise initializers
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:
=
syntax, e.g.,@Lazy var i = 17
, orwrapper type has an
init(wrappedValue:)
.Otherwise, the memberwise initializer parameter will have the same
type as the wrapper. For example:
Codable, Hashable, and Equatable synthesis
Synthesis for
Encodable
,Decodable
,Hashable
, andEquatable
use the backing storage property. This allows property wrapper types to determine their own serialization and equality behavior. For
Encodable
andDecodable
, the name used for keyed archiving is that of the original property declaration (without the_
).$ identifiers
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 projection property), and Swift code can reference those properties. However, Swift code cannot declare any new entities with an identifier that begins with$
. For example:Projections
A property wrapper type can choose to provide a projection property (e.g.,
$foo
) to expose more API for each wrapped property by defining aprojectedValue
property.As with the
wrappedValue
property andinit(wrappedValue:)
, theprojectedValue
property must have thesame access level as its property wrapper type. For example:
When we use the
LongTermStorage
wrapper, it handles the coordination with theStorageManager
and provides either direct access or anUnsafeMutablePointer
with which to manipulate the value:The projection property has the same access level as the original property:
is translated into:
Note that, in this example,
$someValue
is not writable, becauseprojectedValue
is a get-only property.When multiple property wrappers are applied to a given property, only the outermost property wrapper's
projectedValue
will be considered.Restrictions on the use of property wrappers
There are a number of restrictions on the use of property wrappers when defining a property:
enum
.lazy
,@NSCopying
,@NSManaged
,weak
, orunowned
.@Lazy var (x, y) = /* ... */
is ill-formed).wrappedValue
property and (if present)init(wrappedValue:)
of a property wrapper type shall have the same access as the property wrapper type.projectedValue
property, if present, shall have the same access as the property wrapper type.init()
initializer, if present, shall have the same access as the property wrapper type.Impact on existing code
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 aCopying
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 ofinitialization expressions_. However, it may still make sense to
introduce a
Lazy
property wrapper type to cover many of the commonuse cases, leaving the more-magical
lazy
as a backward-compatibilityfeature.
Backward compatibility
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.
Alternatives considered
Composition
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 anAB
wrapper:The main benefit of this approach is its predictability: the author of
AB
decides how to best achieve the composition ofA
andB
, 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 composeA
andB
via@A @B
, how do I know to go look for the manually-composed property wrapper typeAB
? Or maybe that should beBA
?Composition via nested type lookup
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 typeB
insideA
to find the wrapper type: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 typeB
inA
, the composition is invalid. If bothA.B
andB.A
exist, we have a choice: either enforce commutative semantics as part of the language (B.A
andA.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.
Composition without nesting
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 bothA
andB
without producing a nested type likeA<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 declarationthe
Int
value is conceptually wrapped by a property wrapper type, and the property wrapper type'swrappedValue
property guards access to that (conceptual)Int
value. ThatInt
value cannot be wrapped both by instances of bothA
andB
without either duplicating data (bothA
andB
have a copy of theInt
) 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.Using a formal protocol instead of
@propertyWrapper
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:
There are a few issues here. First, a single protocol
PropertyWrapper
cannot handle all of the variants ofwrappedValue
thatare implied by the section on mutability of properties with wrappers,
because we'd need to cope with
mutating get
as well asset
andnonmutating set
. Moreover, protocols don't support optionalrequirements, like
init(wrappedValue:)
(which also has twoforms: one accepting a
Value
and one accepting an@autoclosure () -> Value
) andinit()
. To cover all of these cases, we would need aseveral 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
.Kotlin-like
by
syntaxA previous iteration of this proposal (and its implementation) used
by
syntax similar to that of Kotlin's delegatedproperties, where the
by
followed the variable declaration. For example:There are some small advantages to this syntax over the attribute formulation:
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)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 theby
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.Alternative spellings for the
$
projection propertyThe prefix
$
spelling for the projection property has been the source ofmuch debate. A number of alternatives have been proposed, including longer
#
-based spellings (e.g.,#storage(of: foo)
) and postfix$
(e.g.,foo$
). The postfix$
had the most discussion, based on the idea that it opens up more extension points in the future (e.g.,foo$storage
could refer to the backing storage,foo$databaseHandle
could refer to a specific "database handle" projection for certain property wrappers, etc.). However, doing so introduces yet another new namespace of names to the language ("things that follow$
) and isn't motivated by enough strong use cases.The 2015-2016 property behaviors design
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:
[behavior]
syntax, rather than the attribute syntax described here. See the
property behaviors proposal for more information.
had a new kind of declaration (introduced by the
behavior
keyword). Having a new kind of declaration allowed forthe 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.
didChange
example from the property behaviors proposal).the
self
of their enclosing type. This eliminates some use cases(e.g., implementing a
Synchronized
property wrapper type thatuses a lock defined on the enclosing type), but simplifies the
design.
can use the
$
-prefixed name to refer to the storage property.These were future directions in the property behaviors proposal.
Future Directions
Finer-grained access control
By default, the synthesized storage property will have
private
access, and the projection property (when available) will have the same access as the original wrapped property. 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 toprivate(set)
: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:
The two features could also be combined, allowing property wrapper types to provide the default behavior and the
access-level(...)
syntax to change the default. The current proposal's rules are meant to provide the right defaults while allowing for a separate exploration into expanding the visibility of the synthesized properties.Referencing the enclosing 'self' in a wrapper type
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:This "broadcast a notification that the value has changed" implementation cannot be cleanly factored into a property wrapper type, because it needs access to both the underlying storage value (here,
backingMyVar
) and theself
of the enclosing type. We could require a separate call to register theself
instance with the wrapper type, e.g.,However, this means that one would have to manually call
register(_:)
in the initializer forMyClass
:This isn't as automatic as we would like, and it requires us to have a separate reference to the
self
that is stored withinObservable
. Moreover, it is hiding a semantic problem: the observer code that runs in thebroadcastValueWillChange(newValue:)
must not access the synthesized storage property in any way (e.g., to read the old value throughmyVal
or subscribe/unsubscribe an observer via_myVal
), because doing so will trigger a memory exclusivity violation (because we are callingbroadcastValueWillChange(newValue:)
from within the a setter for the same synthesized storage property).To address these issues, we could extend the ad hoc protocol used to access the storage property of a
@propertyWrapper
type a bit further. Instead of awrappedValue
property, a property wrapper type could provide a staticsubscript(instanceSelf:wrapped:storage:)
that receivesself
as a parameter, along with key paths referencing the original wrapped property and the backing storage property. For example:The (generic) subscript gets access to the enclosing
self
type via its subscript parameter, eliminating the need for the separateregister(_:)
step and the (type-erased) storage of the outerself
. The desugaring withinMyClass
would be as follows:The design uses a
static
subscript and provides key paths to both the original property declaration (wrapped
) and the synthesized storage property (storage
). A call to the static subscript's getter or setter does not itself constitute an access to the synthesized storage property, allowing us to address the memory exclusivity violation from the early implementation. The subscript's implementation is given the means to access the synthesized storage property (via the enclosingself
instance andstorage
key path). In ourObservable
property wrapper, the static subscript setter performs two distinct accesses to the synthesized storage property viaobserved[keyPath: storageKeyPath]
:In between these operations is the broadcast operation to any observers. Those observers are permitted to read the old value, unsubscribe themselves from observation, etc., because at the time of the
broadcastValueWillChange(newValue:)
call there is no existing access to the synthesized storage property.There is a secondary benefit to providing the key paths, because it allows the property wrapper type to reason about its different instances based on the identity of the
wrapped
key path.This extension is backward-compatible with the rest of the proposal. Property wrapper types could opt in to this behavior by providing a
static subscript(instanceSelf:wrapped:storage:)
, which would be used in cases where the property wrapper is being applied to an instance property of a class. If such a property wrapper type is applied to a property that is not an instance property of a class, or for any property wrapper types that don't have such a static subscript, the existingwrappedValue
could be used. One could even allowwrappedValue
to be specified to be unavailable within property wrapper types that have the static subscript, ensuring that such property wrapper types could only be applied to instance properties of a class:The same model could be extended to static properties of types (passing the metatype instance for the enclosing
self
) as well as global and local properties (no enclsoingself
), although we would also need to extend key path support to static, global, and local properties to do so.Delegating to an existing property
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.:
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
.Revisions
Changes from the third reviewed version
init(initialValue:)
has been renamed toinit(wrappedValue:)
to match the name of the property.Changes from the second reviewed version
_
and is alwaysprivate
.wrapperValue
property has been renamed toprojectedValue
to make it sufficiently different fromwrappedValue
. This also gives us the "projection" terminology to talk about the$
property.$foo
) always has the same access as the original wrapped property, rather than being artificially limited tointernal
. This reflects the idea that, for property wrapper types that have a projection, the projection is equal in importance to the wrapped value.Changes from the first reviewed version
init()
, properties that use that wrapper type will be implicitly initialized viainit()
.get
orset
declared, to match with the behavior of existing, similar features (lazy
,@NSCopying
).final
.private
.wrapperValue
, the (computed)$
variable isinternal
(at most) and the backing storage variable gets the prefix$$
(and remains private)._*[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.wrappedValue
property is used as part of this inference. See the "Type inference" section.value
property towrappedValue
to avoid conflicts.@Clamping
example.Acknowledgments
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.