15

When I trying to change the ID property of the byValueObj instance, I received an error that told me I cannot assign to the property of a constant, even though the property is a variable. However, I can do it on a class instance. I kind of knowing that it maybe has something to do with the by value and by reference mechanism. But I don't have a very clear and correct understanding of it. Can someone explain it for me? Thanks.

struct CreatorValue{
    var ID = 2201
}
class CreatorRefer{
    var ID = 2203
}

let byValueObj = CreatorValue()
let byReferObj = CreatorRefer()

byValueObj.ID = 201 //Error: cannot assign to property: 'byValueObj' is a 'let' constant
byReferObj.ID = 203 //works fine here
SLN
  • 4,772
  • 2
  • 38
  • 79

2 Answers2

21

Structures in Swift are value types – and, semantically speaking, values (i.e 'instances' of value types) are immutable.

A mutation of a value type, be it through directly changing the value of a property, or through using a mutating method, is equivalent to just assigning a completely new value to the variable that holds it (plus any side effects the mutation triggered). Therefore the variable holding it needs to be a var. And this semantic is nicely showcased by the behaviour of property observers around value types, as iGodric points out.

So what this means is that you can think of this:

struct Foo {
    var bar = 23
    var baz = 59
}

// ...

let foo = Foo()
foo.bar = 7 // illegal

as doing this:

let foo = Foo()

var fooCopy = foo // temporary mutable copy of foo.

fooCopy.bar = 7   // mutate one or more of the of the properties

foo = fooCopy     // re-assign back to the original (illegal as foo is declared as
                  // a let constant)

And as you can clearly see – this code is illegal. You cannot assign fooCopy back to foo – as it's a let constant. Hence, you cannot change the property of a value type that is declared as a let, and would therefore need make it a var.

(It's worth noting that the compiler doesn't actually go through this palaver; it can mutate the properties of structures directly, which can be seen by looking at the SIL generated. This doesn't change the semantics of value types though.)


The reason you can change a mutable property of a let constant class instance, is due to the fact that classes are reference types. Therefore being a let constant only ensures that the reference stays the same. Mutating their properties doesn't in any way affect your reference to them – you're still referring to the same location in memory.

You can think of a reference type like a signpost, therefore code like this:

class Foo {
    var bar = 23
    var baz = 59
}

// ...

let referenceToFoo = Foo()

you can think of the memory representation like this:

|    referenceToFoo     |  --->  | Underlying Foo instance |
| (a reference to 0x2A) |        |<----------------------->|
                                 |0x2A       |0x32         |0x3A
                                 |  bar: Int |  baz : Int  |
                                 |     23    |      59     |

And when you mutate a property:

referenceToFoo.bar = 203

The reference (referenceToFoo) itself isn't affected – you're still pointing to the same location in memory. It's the property of the underlying instance that's changed (meaning the underlying instance was mutated):

|    referenceToFoo     |  --->  | Underlying Foo instance |
| (a reference to 0x2A) |        |<----------------------->|
                                 |0x2A       |0x32         |0x3A
                                 |  bar: Int |  baz : Int  |
                                 |    203    |      59     |

Only when you attempt to assign a new reference to referenceToFoo will the compiler give you an error, as you're attempting to mutate the reference itself:

// attempt to assign a new reference to a new Foo instance to referenceToFoo.
// will produce a compiler error, as referenceToFoo is declared as a let constant.
referenceToFoo = Foo()

You would therefore need to make referenceToFoo a var in order to make this assignment legal.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Nice explanation! Thank you so much") – SLN Jun 24 '16 at 13:40
  • @SLN Happy to help :) – Hamish Jun 24 '16 at 13:40
  • 1
    @SLN Just FYI – I did quite a hefty edit to this answer today to reflect the fact that structs can in fact be mutated directly (without copying). I hope you have the time to look over the changes, and I'm happy to answer any questions if you have any :) – Hamish Jan 02 '17 at 18:58
  • I did read your edit answers. Thanks a lot for your updating after nearly 6 months. BTW happy new year :) – SLN Jan 02 '17 at 20:48
  • 1
    @SLN Well whatdoya know – I made another mutation (ha!) to my answer, I think I'm now finally happy with it. Really I think I just got bogged down with the implementation details of what the compiler does rather than just focussing on the important semantics of value types. As always, happy to answer questions about the edit, and sorry for bothering you again! – Hamish Nov 09 '17 at 00:25
2

struct is a value type. If you edit them you are calling the default setter for the property which is nothing but a mutating method, which is nothing but a static method of the struct which has self as the first argument as inout which returns the method (Swift for now has curry syntax for unapplied method calls, but will change that to a flattened one). Just as a side note: When the method is not mutating it will not be inout.

Because inout is working by copy in - copy out, didSet is called, even if nothing changed.

“If you pass a property that has observers to a function as an in-out parameter, the willSet and didSet observers are always called.” Excerpt From: Apple Inc. “The Swift Programming Language (Swift 3).” iBooks. https://itun.es/de/jEUH0.l

Code that a var struct will be copied when a property is mutated:

struct Size {
    var width: Int
    var height: Int
}

struct Rectangle {
    var size: Size
}

var screenResolution = Rectangle.init(size: Size.init(width: 640, height: 480)) {
    didSet {
        print("Did set the var 'screenResolution' to a new value! New value is \(screenResolution)")
    }
}

screenResolution.size.width = 800

Calls the didSet even though we only mutated a the Int in the property of the struct.

If it would be complete new copy then you would expect the mutated struct to be a new copy with new memory allocation. But this is not what happens in the example, see code below.

// Value of a pointer is the address to the thing it points to
internal func pointerValue(of pointer: UnsafeRawPointer) -> Int {
    return unsafeBitCast(pointer, to: Int.self)
}

internal struct MyStruct {
    internal var a: Int
}

internal var struct1: MyStruct = MyStruct.init(a: 1)
pointerValue(of: &struct1) // output: 4405951104

struct1.a = 2
pointerValue(of: &struct1) // output: 4405951104

So the structure is not copied. But because it is inout:

“Write your code using the model given by copy-in copy-out, without depending on the call-by-reference optimization, so that it behaves correctly with or without the optimization.” Excerpt From: Apple Inc. “The Swift Programming Language (Swift 3).” iBooks. https://itun.es/de/jEUH0.l


Code example with inout only:

struct MyType {
    var x: Int
    mutating func m1(y: Int) -> Int {
        x += 1
        return x + y
    }

}

let mytypem1: (inout MyType) -> (Int) -> Int = MyType.m1
var myType = MyType.init(x: 1) // has to be "var"
mytypem1(&myType)(2) // returns 3
Binarian
  • 12,296
  • 8
  • 53
  • 84
  • @Hamish Read carefully. I only write that the struct is not on a new memory address of a copy, not that there no copying taking place. Yes I can not check if there was a temporary struct. And I also admit that the theory of a copy is made, mutated and then set to the same address sounds very plausible (which was your theory in the first place). – Binarian Oct 04 '16 at 20:57
  • 2
    Turns out we were both wrong – I was looking at the SIL today (see [this example gist](https://gist.github.com/hamishknight/a369243158e875e5ea9d1a6e22212802)), and it turns out that structs can in fact be mutated directly (without copying). In most cases, the address of the property is simply looked up and written to directly. – Hamish Jan 02 '17 at 18:54
  • @Hamish I say that it is inout, which does have an optimization which does not copy but reference the argument. So changing it directly could be because in your code it is optimized to directly change it instead of copying it. – Binarian Jan 03 '17 at 18:06
  • 1
    Just tried emitting the *raw* SIL (pre any optimisation), and the struct is still able to be mutated directly :) – Hamish Jan 03 '17 at 18:18
  • @Hamish You are right, structures does not need to be copied behind the curtain when a property changes. But because it is `inout`, the observer has to be called and from a the perspective of Swift high level semantics you should work as if it was copied. Apple to inout: “Write your code using the model given by copy-in copy-out, without depending on the call-by-reference optimization, so that it behaves correctly with or without the optimization.” – Binarian Jan 04 '17 at 09:07
  • 1
    You're right that the setter uses `@inout` for the `self` parameter – although from what I can tell from the emitted raw SIL, the setter for a value type stored property will only be referenced in the case of supplying a `didSet`/`willSet` observer or when using protocols. So in most cases, the property setter isn't even relevant. Also note that from what I can tell, the promotion of `inout` parameters from copy-in copy-out to pass by reference appears to happen between the raw SIL and canonical SIL, when 'guaranteed optimisations' happen – so will happen even in -Onone builds. – Hamish Jan 06 '17 at 19:08
  • @Hamish Just because the implementation detail does not respect the immutability by design in all cases, does not mean you should from a high level view. Therefore I tried to stay in that view for my explanation. `you always get a unique, copied instance, you can trust that no other part of your app is changing the data under the covers` https://developer.apple.com – Binarian Jan 11 '17 at 13:14
  • 1
    Oh I certainly agree that from a high level view, it's better to think of value semantics as copying rather than directly mutating – sorry, I was just getting carried away (probably too much) by the SIL :) When I get time, I'll add an addendum to my answer to include these details so we can clean up the comments. – Hamish Jan 13 '17 at 19:26
  • @Hamish But nonetheless very interesting how Swift handles this situation under the hood now. I do think the SIL generated code from the Swift code could change anytime. – Binarian Jan 13 '17 at 20:59