Swift Value and Reference type semantics - Structs, Classes, Struct inside Class, Class inside Struct

Swift Value and Reference type semantics - Structs, Classes, Struct inside Class, Class inside Struct

Long ago Swift introduced a nice concept differentiating between value and reference semantic using Struct and Classes. As we all know, Struct is a value semantic, which means when value of one struct is assigned to another struct, only value is copied, not the reference - Which in other words means changing the value of original struct has no effect on the value of another struct to which it was assigned.

On the other hand, classes follow reference semantics. When reference is assigned to another reference type, the reference to these constructs is copied, and not the value itself. Which means changing the value of first reference type will also result in change in value of the second reference to which assignment was made.

I know, I know this theory sounds confusing. So let's start taking a look at some examples to clearly understand how do they exactly work going step by step

  • Just Structs

We are going to create a simple struct called Person with just couple of properties.

struct Person {
    var name: String
    var age: Int
}

Now to demonstrate the concept, let's go ahead and create two instances of type Person

let person1 = Person(name: "foo", age: 40)
let person2 = Person(name: "bar", age: 50)

First thing you will note is that since both instances are lets, neither can you reassign any of them nor can you change their properties - Even though properties are vars.

So if you want to assign one instance to other or change its property, you will have to change make them variables.


To go the next concept, let's change both the instances from let to var and see how does it change the memory semantics.

var person1 = Person(name: "foo", age: 40)
var person2 = Person(name: "bar", age: 50)

To prove the value semantics point I made, let's try to assign the value of person2 to person1 and change the value of person2 afterwards and verify if it results in change in property value for person1

person1 = person2

person2.name = "foobar"
person2.age = 80

// Prints
// Person 1 name bar
// Person 1 age 50

print("Person 1 name \(person1.name)")
print("Person 1 age \(person1.age)")

// Prints
// Person 2 name foobar
// Person 2 age 80

print("Person 2 name \(person2.name)")
print("Person 2 age \(person2.age)")

As you can see, even though we assigned the person2 to person1, change in the value of person2 did not affect the property values of person1.

This is the significance of using struct types over classes that it merely copies values without worrying anything about references. Imagine passing this value to any function or thread and it getting modified without the caller's knowledge. How frustrating would that be?


Does Swift really implement Copy-on-write for struct types?

I was always under impression that after assignment, Swift stores both struct instances at the same memory location until one of the struct undergoes mutation. Apparently this is not the case or this optimization does not take place all the time.

To prove it, I created two structs, assigned one to the other and immediately printed their memory addresses  - And to my surprise and astonishment, they were both different. Huh!

var person1 = Person(name: "foo", age: 40)
var person2 = Person(name: "bar", age: 50)

person1 = person2

// Prints - 0x7ffee039a6d8
print(NSString(format: "%p", address(o: &person1)))

// Prints - 0x7ffee039a6c0
print(NSString(format: "%p", address(o: &person2)))

func address(o: UnsafePointer<Void>) -> Int {
    return unsafeBitCast(o, to: Int.self)
}


Caveat:

One caveat though while using struct. Swift creates the whole new copy of struct when original value is modified. So if you are using too many structs with large size, this can put an excessive strain on available resources.

  • Just Classes

After looking at how structs work, let's take a look at how memory semantics work for Classes in Swift.

First off, we are going to create a simple class named Car

class Car {
    var company: String
    var years: Int

    init(company: String, years: Int) {
        self.company = company
        self.years = years
    }
}

Next is the time to create its two instances,

let car1 = Car(company: "BMW", years: 10)
let car2 = Car(company: "Mercedes", years: 15)

Now when you try to assign the references or change its properties, you will notice one difference between how it behaved with structs vs classes

Since both the instances are let constants, we cannot change their references. But irrespective whether they are constants or variables, we can still modify their properties as long as properties are vars

Now let's change both variable from constants to variables and modify the original instance to see what happens to second instance,

var car1 = Car(company: "BMW", years: 10)
var car2 = Car(company: "Mercedes", years: 15)

// Before this assignment, both car1 and car2 point to different memory locations
// car1 : 0x600003d2d470
// car2 : 0x600003d2d4a0 
car1 = car2

// After assignment, car1 will point to memory location of car2
// car1 : 0x600003d2d4a0
// car2 : 0x600003d2d4a0

// When we change properties associated with car2, it will also change same properties of car1 since both of them point to the same memory address

car2.company = "ddd"
car2.years = 100

// Prints
// Car 1 company ddd
// Car 1 years 100

print("Car 1 company \(car1.company)")
print("Car 1 years \(car1.years)")


// Prints
// Car 2 company ddd
// Car 2 years 100 

print("Car 2 company \(car2.company)")
print("Car 2 years \(car2.years)")

As you can see, it's often dangerous and deceptive to use this kind of assignment with reference types such as classes. Since we do not necessarily want changes to one instance to automatically propagate to another instance

Implementing copy for reference types

If you want to copy value of all the properties associated with first instance to the second instance without having both of them point to the same memory location, you can do so by conforming to NSCopying protocol and implementing the copy method.

extension Car: NSCopying {
    func copy(with zone: NSZone? = nil) -> Any {
        let newCar = Car(company: self.company, years: self.years)
        return newCar
    }
}

car1 = car2.copy() as! Car

// car1 - 0x60000056b750
// car2 - 0x60000056b720

car2.company = "ddd"
car2.years = 100

// Prints
// Car 1 company Mercedes
// Car 1 years 15

print("Car 1 company \(car1.company)")
print("Car 1 years \(car1.years)")

// Prints
// Car 2 company ddd
// Car 2 years 100

print("Car 2 company \(car2.company)")
print("Car 2 years \(car2.years)")


  • Class inside Struct (!! Danger !!)

This is more interesting case than the ones that we saw earlier. How does Swift behave when classes are stored inside struct? Let's combine our previous examples to create a nested structure,

struct Person {
    var name: String
    var age: Int
    var car: Car
}
var personWithCar1 = Person(name: "foo", age: 40, car: Car(company: "bmw", years: 10))
var personWithCar2 = Person(name: "bar", age: 50, car: Car(company: "mercedes", years: 20))

personWithCar1 = personWithCar2

When personWithCar2 is assigned to personWithCar1 in above code sample, all the value types in struct are safe. However, the danger arises when we are dealing with reference types inside struct.

When nested struct gets assigned to another struct instance, the reference type DO NOT GET COPIED, but rather they both point to the same memory address.

In other words, when following assignment takes place,

personWithCar1 = personWithCar2

personWithCar1.car and personWithCar2.car point to the same memory location. Means changing the property value of car associated with one struct will also change the properties of car on other struct,

Please note that value types inside struct have no effect of such assignment. They will act the same way as if they were standalone value types

var personWithCar1 = Person(name: "foo", age: 40, car: Car(company: "bmw", years: 10))
var personWithCar2 = Person(name: "bar", age: 50, car: Car(company: "mercedes", years: 20))

// Before assignment
// personWithCar1.car : 0x600001102670
// personWithCar2.car : 0x600001102970

personWithCar1 = personWithCar2

// After assignment
// personWithCar1.car : 0x600001102970
// personWithCar2.car : 0x600001102970

personWithCar2.car.company = "toyota"
personWithCar2.car.years = 5

// Prints
// PersonWithCar1 -> car -> company toyota
// PersonWithCar1 -> car -> years 5

print("PersonWithCar1 -> car -> company \(personWithCar1.car.company)")
print("PersonWithCar1 -> car -> years \(personWithCar1.car.years)")

// Prints
// PersonWithCar2 -> car -> company toyota
// PersonWithCar2 -> car -> years 5


print("PersonWithCar2 -> car -> company \(personWithCar2.car.company)")
print("PersonWithCar2 -> car -> years \(personWithCar2.car.years)")

  • Struct inside Class

Now we will take a look at the last case - How does memory semantics work if we have struct inside the class? Let's extend our struct to look like this,

class Car {
    var company: String
    var years: Int

    var person: Person

    init(company: String, years: Int, person: Person) {
        self.company = company
        self.years = years
        self.person = person
    }
}

Now let's start creating the instances of Car and see how it affects its properties after assignment,

var carWithPerson1 = Car(company: "bmw", years: 20, person: Person(name: "foo", age: 40))
var carWithPerson2 = Car(company: "mercedes", years: 10, person: Person(name: "bar", age: 80))

carWithPerson1 = carWithPerson2

After assignment, let's modify the value type (struct) associated with reference type (class) and see if it has any effect on struct inside other class,

carWithPerson1.person.name = "nothing"
carWithPerson1.person.age = 700

// Or alternatively this will have the same effect as last two lines
// carWithPerson1.person = Person(name: "nothing", age: 700)

Since Car is a reference type and both carWithPerson1 and carWithPerson2 point to the same memory address, it will also apply to all the properties associated with this Car object housing Person.

So any change in properties of one class (Including complex types such as structs) will also reflect in change in values in properties of other class pointing to same memory address.


carWithPerson1.person = Person(name: "nothing", age: 700)

// Prints
// CarWithPerson1 -> person -> name nothing
// CarWithPerson1 -> person -> age 700

print("CarWithPerson1 -> person -> name \(carWithPerson1.person.name)")
print("CarWithPerson1 -> person -> age \(carWithPerson1.person.age)")

// Prints
// CarWithPerson2 -> person -> name nothing
// CarWithPerson2 -> person -> age 700

print("CarWithPerson2 -> person -> name \(carWithPerson2.person.name)")
print("CarWithPerson2 -> person -> age \(carWithPerson2.person.age)")

However, this might make you all think that person object associated with both Cars point to the same memory address. But these being structs, will still point to different memory addresses, (But will still have the same data)

Here's the brief summary of this article for easy reference,

  1. Struct
  • Assignment from one struct to another will still store both instances at the same memory address (Some cases not - As explained above).
  • When one struct is modified, Swift memory management will store them into separate memory locations
  • The modification in property of first struct will not affect the other since the assignment simply copies the values

2. Class

  • Assignment from one class to another will copy the reference in such a way that both class instances will now point to the same memory address
  • When one class instance is modified, the other instance also gets affected
  • To avoid unwanted mutation, call the copy on assignment so that the copy is assigned to another struct instead of reference to memory address

3. Class inside Struct

  • Assignment from one struct housing the class to another similar struct will keep value types in their own memory locations, but will treat the nested reference types such that they all point to same memory address
  • After assignment all the reference types inside value types will point to same memory address
  • Modification to any property of reference type inside value type will also reflect into the change in reference type inside the other value type

4. Struct inside Class

  • When value type (Struct) inside the reference type - Class gets assigned to another reference type, they both point to the same memory address
  • Due to the reference to the same memory address, any change in properties associated with this enclosing type also affects other reference type to which first reference has been assigned
  • Any change in value of struct inside class will also reflect in change in value of struct type inside other class
  • However, even though struct values get transferred from one reference type to another, this enclosed structs still point to different memory locations

So that's all folks for today. I hope this was a useful concept for many of you. It's surprising how apparently simple question like this can have intriguing answers. If you still have any questions or find any mistake in the article, please let me know in comment box or you can also reach out to me on my @jayeshkawli Twitter handle