Why constant constraints the property from a structure instance but not the class instance?
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
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.
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