3

I was surprised to find that assigning a member of a value type through either a subscript operation or computed property in Swift worked as one would expect for a reference type: e.g. I really expected myArrayOfValueType[0].someField = value would be either disallowed or a no-op as it would just assign to a copy that is discarded. But in fact what it does is call both the getter and the setter: performing the mutation and then assigning the value type back automatically.

My question is: Where is this behavior documented? Can we rely on this behavior?

struct Foo {
    var a : Int = 1
}
struct FooHolder {
    var foo :  Foo = Foo()

    var afoo : Foo {
        get { return foo }
        set { foo = newValue }
    }
    subscript(i: Int) -> Foo {
        get { return foo }
        set { foo = newValue }
    }
}
var fh = FooHolder()
fh.afoo.a // 1
fh.afoo.a = 42 // equivalent: var foo = fh.afoo; foo.a = 42; fo.afoo = foo
fh.afoo.a // 42!

// same is true of subscripts
var fh = FooHolder()
fh[0].a // 1
fh[0].a = 42
fh[0].a // 42!

EDIT:

To state the question in another way:

Swift makes both subscript and computed property access look transparent as far as value type copying is concerned. Where a regular method call would return a copy of a value type Swift appears to perform a two-step dance using both the getter and setter at different phases of evaluation to produce the value, mutate it, and then set it back. This seems like something that would/should be documented if for no other reason than it is totally non-obvious and could even have side effects in poorly written code.

Pat Niemeyer
  • 5,930
  • 1
  • 31
  • 35
  • 1
    I pasted that code into a playground, got "Type 'FooHolder' has no subscript members" – Craig Grummitt Jan 15 '16 at 23:23
  • Pat, please. refine you question! Your code will not compile and your question can be interpreted, that we can read or write to / from properties with help of subscript. Your struct FooHolder doesn't conform to protocol CollectionType nor protocol Indexable. – user3441734 Jan 16 '16 at 00:38
  • Thanks. I have fixed the example above. As you could probably infer I originally showed both the property and the (very similar) subscript operator. – Pat Niemeyer Jan 16 '16 at 01:40

2 Answers2

1

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:

  1. Basic value type data structures should be as usable and accessible as they are in other C-family languages.
  2. 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 CGRects 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.)

rickster
  • 124,678
  • 26
  • 272
  • 326
  • Please, did you try the code included in Pat's question? Can you explain us what the statement fh[42].a means there? I see there nothing which has a subscript, nothing to conform to protocol CollectionType or protocol Indexable. His code simply not compiles! He wrote "... assigning a member of a value type through either a subscript operation or computed property ...", can you explain us that sentence? How can i assign a member of a value type through either a subscript operation or computed property? – user3441734 Jan 16 '16 at 00:33
  • Sorry, fixed the code. It was just showing the same thing with subscripts. – Pat Niemeyer Jan 16 '16 at 01:46
  • "How it works is a case of the Swift compiler doing more than the C compiler". Yes, exactly - so in most languages the grammar used in the compiler would resolve the left hand side expression to a target but since Swift has to deal with these get/set abstractions it does something new (to me anyway) and invokes both the getter and the setter at different phases in the evaluation. I was surprised by this and just thought it must be documented somewhere. – Pat Niemeyer Jan 16 '16 at 01:56
  • @PatNiemeyer aha, now i am commencing to see what we are talking about :-) Almost everybody coming to Swift from C or Objective-C world has had the same trouble, no exceptions!. Your example (question) is clear now and it works exactly as expected and as described in apple docs. – user3441734 Jan 16 '16 at 02:13
0

value types are copied by default

struct Foo {
    var a : Int = 1
}
struct FooHolder {
    var foo :  Foo = Foo()
    var afoo : Foo {
        get { return foo }
        set { foo = newValue }
    }
}
var fh = FooHolder()

// arr1 and arr2 DOESN'T share the same copy of fh
var arr1 = [fh]
arr1[0].foo.a = 0
var arr2 = [fh]
arr2[0].foo.a = 200

// foo is a new copy of arr1[0].afoo (Foo)
var foo = arr1[0].afoo // Foo(a: 1)
foo.a = 100
print(arr1,arr2,fh,foo)
// [FooHolder(foo: Foo(a: 0))] [FooHolder(foo: Foo(a: 200))] FooHolder(foo: Foo(a: 1)) Foo(a: 100)

arr1[0].foo = foo
print(arr1,arr2,fh,foo)
// [FooHolder(foo: Foo(a: 100))] [FooHolder(foo: Foo(a: 200))] FooHolder(foo: Foo(a: 1)) Foo(a: 100)

if you have different behavior, check you Swift installation (version, OS ...) and fill a radar.

to see that it works as expected, see next example

protocol P {
    var i: Int {get set}
}
struct S:P {
    var i:Int
}
class C:P {
    var i: Int
    init(i:Int){
        self.i = i
    }
}

var s = S(i: 0)
var c = C(i: 0)
var arr1:[P] = [s,c]
var arr2:[P] = [s,c]
arr2[0].i = 20
arr2[1].i = 200
dump(arr1)
/*
▿ 2 elements
  ▿ [0]: S
    - i: 0
  ▿ [1]: C #0
    - i: 200
*/
dump(arr2)
/*
▿ 2 elements
  ▿ [0]: S
    - i: 20
  ▿ [1]: C #0
    - i: 200
*/

This is part of Swift's language definition, mentioned in apple docs at a lot of places and this behavior MUST be independent on Swift' platform.

user3441734
  • 16,722
  • 2
  • 40
  • 59