ref – https://www.raywenderlich.com/112029/reference-value-types-in-swift-part-2
Mixing Value and Reference Types
You’ll often run into situations where reference types need to contain value types, and vice versa. This can easily complicate the expected semantics of the object.
To see some of these complications, you’ll look at an example of each scenario.
Reference Types Containing Value Type Properties
It’s quite common for a reference type to contain value types. An example would be a Person class where identity matters, that stores an Address structure where equality matters.
To see how this may look, replace the contents of your playground with the following basic implementation of an address:
1 2 3 4 5 6 |
struct Address { var streetAddress: String var city: String var state: String var postalCode: String } |
All properties of Address together form a unique physical address of a building in the real world. The properties are all value types represented by String; the validation logic has been left out to keep things simple.
Next, add the following code to the bottom of your playground:
1 2 3 4 5 6 7 8 9 10 |
class Person { // Reference type var name: String // Value type var address: Address // Value type, own copy init(name: String, address: Address) { self.name = name self.address = address } } |
This mixing of types makes perfect sense in this scenario. Each class instance has its own value type property instances that aren’t shared. There’s no risk of two different people sharing and unexpectedly changing the address of the other person.
To verify this behavior, add the following to the end of your playground
1 2 3 4 5 6 7 8 9 10 11 12 |
// 1 let kingsLanding = Address(streetAddress: "1 King Way", city: "Kings Landing", state: "Westeros", postalCode: "12345") let madKing = Person(name: "Aerys", address: kingsLanding) let kingSlayer = Person(name: "Jaime", address: kingsLanding) // 2 kingSlayer.address.streetAddress = "Secret Address where the King can't find him" // 3 madKing.address.streetAddress // 1 King Way kingSlayer.address.streetAddress // Secret Address where the King can't find him |
Value Types Containing Reference Type Properties
Add the following code to your playground to demonstrate a value type containing a reference type:
1 2 3 4 |
struct Bill { let amount: Float let billedTo: Person // this one will be shared by numerous Bill instances because it is a reference. } |
Each copy of Bill is a unique copy of the data, but the billedTo Person object will be shared by numerous Bill instances. This simply does not make sense in maintaining the value semantics of your objects.
Using Copy-on-Write Computed Properties
Swift can give you the ability for your value type to spawn its own reference type. That way, when other objects wants to reference your value type’s reference object, it won’t simply point to it…as that would make one data object be changed by 2 or more references.
When other objects point to your value type’s reference object, your value type will spawn (create another instance) of the reference object for others. That way, your value type will have its own reference object with its respective address, and other object would have their reference object with its respective address.
1) declare a reference type
2) create a property (billedToForRead) to get the current reference
3) create another property (billedToCreateNewInstance) which re-assigns the reference to a new heap allocated Person object. When we create a new object on the heap, our property billedTo will re-point to that object. Thus, billedToCreateNewInstance will return a new object with a new address.
In Swift, we need to use the word mutating in order to let the value type know that its property will be changing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// a value type struct Bill { let amount: Float private var _billedTo: Person // 1, points to reference type, shared by many references // 2, use (property name)ForRead to implement, read operations var billedToForRead: Person { print("-- billedToForRead --") return _billedTo } // 3, use (property name)ForWrite to implement, write operations var billedToCreateNewInstance: Person { // in a value type, if you need to modify the properties of your structure // or enumeration within a particular method, you can opt in to mutating behavior // for that method. // (our case) // Mutating a "member of a value type instance" means mutating the value type instance itself (self). // The address will change. mutating get { print("-- billedToForWrite --") print("\(_billedTo)") _billedTo = Person(name: _billedTo.name, address: _billedTo.address) return _billedTo } } init(amount: Float, billedTo: Person) { print("--- init Bill ---") self.amount = amount _billedTo = Person(name: billedTo.name, address: billedTo.address) } } var ricky = Person(name: "Ricky Tsao", address: "Shenzhen, China") var billOne = Bill(amount: 8.88, billedTo: ricky) var check = billOne.billedToForRead var newPerson = billOne.billedToCreateNewInstance print("------ END --------") |
If you can guarantee that your caller will use your structure exactly as you meant, this approach would solve your issue. In a perfect world, your caller would always use billedToForRead to get data from your reference and billedToCreateNewInstance to make a change to the reference.
However, callers will also make some mistakes in calling your API properties. Hence, what we can do is to hide our properties from the outside and create methods to
Defensive Mutating Methods
So as mentioned, we’ll hide the two new properties from the outside.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
// 1) make properties private // 2) create mutating functions // a value type struct Bill { let amount: Float private var _billedTo: Person // make the propertis private private var billedToForRead: Person { print("-- billedToForRead --") return _billedTo } // private var billedToCreateNewInstance: Person { // in a value type, if you need to modify the properties of your structure // or enumeration within a particular method, you can opt in to mutating behavior // for that method. // (our case) // Mutating a "member of a value type instance" means mutating the value type instance itself (self). // The address will change. mutating get { print("-- billedToForWrite --") print("\(_billedTo)") _billedTo = Person(name: _billedTo.name, address: _billedTo.address) return _billedTo } } // mutating means this function will change a property mutating func updateBilledToNewAddress(address: String) { billedToCreateNewInstance.address = address } mutating func updateBilledToName(name: String) { billedToCreateNewInstance.name = name } init(amount: Float, billedTo: Person) { print("--- init Bill ---") self.amount = amount _billedTo = Person(name: billedTo.name, address: billedTo.address) } } var ricky = Person(name: "Ricky", address: "Shenzhen, China") var billOne = Bill(amount: 8.88, billedTo: ricky) billOne.updateBilledToName(name: "Ivan K") billOne.updateBilledToNewAddress(address: "Vision Park, Shenzhen, China") print("------ END --------") |
1) You made both computed properties private so that callers can’t access the properties directly.
2) You also added methods to mutate the Person reference with a new name or address. This makes it impossible for someone else to use it incorrectly, since you’re hiding the underlying billedTo property.
Also, take note that “Cannot use mutating member on immutable value”.
for example:
1 2 3 4 5 |
var ricky = Person(name: "Ricky", address: "Shenzhen, China") let billOne = Bill(amount: 8.88, billedTo: ricky) // notice declared with 'let' billOne.updateBilledToName(name: "Ivan K") // error: Cannot use mutating member on immutable value billOne.updateBilledToNewAddress(address: "Vision Park, Shenzhen") // error: Cannot use mutating member on immutable value |
A More Efficient Copy-on-Write
There is only one small problem. Whenever we want to change the Person object’s attributes, we ALWAYS instantiate a Person object:
1 2 3 4 |
mutating get { _billedTo = Person(name: _billedTo.name, address: _billedTo.address) return _billedTo } |
As you can see, whenever we get the billedTo attribute, we always return a newly instantiated object.
If another Bill object references this Person object, then yes, we should instantiate a new Person object for our own Bill object to reference, and change data with.
However, what if the Person object has no other references on it? ..and is only referenced by self?
In that case, we do not need to instantiate it. We simply use it as it, and change the attributes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
struct Bill { let amount: Float private var _billedTo: Person // make the propertis private private var billedToForRead: Person { print("-- billedToForRead --") return _billedTo } private var billedToCreateNewInstance: Person { mutating get { print(" --- billedToCreateNewInstance --- ") if !isKnownUniquelyReferenced(&_billedTo) { print("Making a copy of _billedTo") _billedTo = Person(name: _billedTo.name, address: _billedTo.address) } else { print("Not making a copy of _billedTo") } return _billedTo } } // mutating means this function will change a property mutating func updateBilledToNewAddress(address: String) { billedToCreateNewInstance.address = address } mutating func updateBilledToName(name: String) { billedToCreateNewInstance.name = name } func getBill() -> Person { return billedToForRead } init(amount: Float, billedTo: Person) { print("--- init Bill ---") self.amount = amount _billedTo = Person(name: billedTo.name, address: billedTo.address) } } var ricky = Person(name: "Ricky", address: "Shenzhen, China") // 1 var billOne = Bill(amount: 8.88, billedTo: ricky) // 2 billOne.updateBilledToName(name: "Ivan K") //3 billOne.updateBilledToNewAddress(address: "Vision Park, Shenzhen") var anotherReference = billOne.getBill() // 4 billOne.updateBilledToName(name: "Stone Z") // 5 billOne.updateBilledToName(name: "Bao'an, Shenzhen, China") print("------ END --------") |
Hence here is what’s happening:
A Person object with “Ricky” (100c01fa0) is allocated on the heap. It is passed into the Bill object (100410438) to be initialized. This initialization is really about the Bill object creating its own instance of Person object (100c01fe0), and setting all its data the same as the passed in “Ricky” Person object. The original “Ricky” Person object created outside is not pointed to, nor affected.
We then want to change the data of our Bill object to Ivan. We do this through the property, and evaluate whether the Person object 100c01fe0 has any other references on it. Uniquely Referenced means it is referenced only by 1 object.
1 2 3 |
if !isKnownUniquelyReferenced(&_billedTo) { } |
Thus, we see that IT IS only referenced once, by our Bill object. Thus, we don’t create an instance and just return the Person object. We then proceed to change the data.
Then say we have another Bill object reference our Person (100520008). Thus, we now have two references pointing to our Person object
Because we have 2 references on the Person object, isKnownUniquelyReferenced will fail, and we create another Person object. That way, we get our own unique instance of Person.