I'm having a hard time finding explicit documentation of this behavior. There's a little bit of hinting at it in Computed Properties in the Swift book, but it's probably worth being stated more upfront. I'd recommend filing a bug against the documentation.
In the meantime — yes, you can rely on this behavior, as it would seem to be pretty fundamental to the design of Swift. You can get this from two pretty safe assumptions about Swift's design philosophy:
- Basic value type data structures should be as usable and accessible as they are in other C-family languages.
- Computed properties should from the caller's perspective behave the same as stored properties.
For #1, consider the following (Obj)C example:
CGRect rect = CGRectMake(0, 0, 320, 480);
// CGRect is a nested structure: {origin: {x, y}, size: {w, h}}
rect.origin.y = 20;
rect.size.height = 460;
// rect is now {{0, 0}, {320, 460}}
This works because a C struct is syntactic sugar for pointer math. A CGRect
is really just a contiguous block of four floating-point values. (As a nested struct, it's a block of two smaller blocks, which themselves are blocks of values.) When you read from or write to origin.y
the compiler uses the struct's definition to determine where in the memory block to read or write a single float, regardless of whether that's memory statically allocated on the stack (a function parameter or local variable) or dynamically allocated on the heap.
Swift needs to be able to work with data structures originating in or passed to C APIs, so you can expect basic value type structures to work pretty much the same as in C, modulo the mutability restriction of var
vs let
. Consider extending the CGRect
example to involve an array of rects:
CGRect rects[10] = /*...*/;
rects[5].size.height = 23;
Again, this "just works" in C because it's just pointer arithmetic handled for you by the compiler. rects
is a contiguous chunk of memory; find the offset into that chunk of the 6th sub-chunk (a rect / block of four floats); find the offset into that sub-chunk of the size
field (a size / block of two floats); find the offset into that of the height
field; write to that location. Swift needs to interoperate with C (not to mention be memory efficient itself), so this "just works" in Swift, too.
For #2, reading between the lines a bit in Computed Properties in the Swift book hints pretty strongly that computed properties should act just like stored properties from the caller's perspective.
To borrow their example, let's extend CGRect
to add a computed center
property:
var center: CGPoint {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return CGPoint(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
If we have our array of CGRect
s in Swift, we should be able to set center
just as we can set origin
:
rects[4].origin.x = 3
rects[6].center.x = 5
And indeed, that works. From the caller's perspective, center
is just another property — they don't have to care whether it's stored or computed.
This is a critical part of abstraction: a protocol would merely declare that these properties exist (and are readwrite or readonly), and two different structs adopting the protocol could implement center
as stored and origin
as computed or vice versa.
How it works is a case of the Swift compiler doing more than the C compiler, but with the same philosophy. Where the C compiler sees accesses to structure members and does pointer math, the Swift compiler sees accesses and inserts function calls that work through pointer indirection with underlying values. It's as if the following functions existed in C:
inline CGPoint CGRectGetCenter(CGRect rect) {
return CGPointMake(rect.origin.x + (size.width / 2),
rect.origin.y + (size.height / 2)
}
inline void CGRectSetCenter(CGRect *rect, CGPoint newCenter) {
rect->origin.x = newCenter.x - (rect->size.width / 2);
rect->origin.y = newCenter.y - (rect->size.height / 2);
}
...and the compiler automagically turned reads and writes to rect.center
to calls to those functions:
CGRect rect = CGRectMake(0, 0, 320, 480);
CGRectGetCenter(rect); // {160, 240}
CGRectSetCenter(&rect, CGPointMake(0, 0));
// rect is now {{-160, -240}, {320, 480}}
(And notice that in C, those functions work regardless of whether the passed rect or pointer-to-rect is in an array or nested structure.)
The "magic" part about Swift is that it applies such transformations all the way down — so if a struct includes another struct, the redirection of computed property accessors through functions works all the way down, so that computed properties work just like stored properties from the caller's perspective.
It even works for inout
function parameters. The +=
and (while it's still around) ++
operators are functions with inout
parameters, so you can do the following:
rects[7].origin.y += 10
rect.center.x++
Each time you use a function with an inout
parameter, the compiler emits the necessary pointer math or function calls to read the current value of the member, calls the function, and then does the reverse set of pointer math / function calls to put the result in place. So rect.center.x++
calls CGRect.center.get
, pokes a value into place in the resulting CGPoint
struct, then calls CGRect.center.set
.
(This part is a bit more thoroughly documented at In-Out BurgersParameters in the Swift book.)