The story of one mother & two sons: value type vs reference type in Swift

Swift, the powerful and versatile programming language pioneered by Apple, can be thought of as a mother who has two very different sons: value types and reference types. While they are both Swift‘s children and share some traits, their unique characteristics and how they handle life‘s challenges set them apart. Let‘s dive into the tale of Swift‘s offspring and explore what defines them.

Identifying the Brothers

Swift‘s firstborn son, Value Type, comes in a variety of forms including structs, enums, and tuples. Here‘s an example of a value type defined as a struct:

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

let person1 = Person(name: "John", age: 30)
let person2 = person1
person2.age = 31

print(person1.age) // Output: 30
print(person2.age) // Output: 31

When an instance of a value type like person1 is assigned to a new constant/variable or passed as an argument to a function, its value is copied rather than just passing a reference. So person1 and person2 are separate, independent instances. Modifying one does not affect the other, as seen in the example above.

In contrast, Swift‘s second son, Reference Type, manifests as classes as well as functions and closures. Here‘s an example of a reference type defined as a class:

class Car {
var make: String
var model: String
var year: Int

init(make: String, model: String, year: Int) {
self.make = make
self.model = model
self.year = year
}
}

let car1 = Car(make: "Tesla", model: "Model 3", year: 2021)
let car2 = car1
car2.year = 2022

print(car1.year) // Output: 2022
print(car2.year) // Output: 2022

When a reference type instance like car1 is assigned to a new constant/variable or passed around, only a reference to the instance is copied while the instance itself lives in a shared memory space. So car1 and car2 both refer to the exact same instance. Modifying the year on car2 also changes it for car1.

Nature vs Nurture

One key difference between the brothers is how they are stored in the family‘s house. Value Type prefers to sleep in his own small room called the stack where he can quickly be tucked in and access his belongings. Reference Type, on the other hand, stores all his stuff in a large shared room called the heap and just keeps a note with directions to his section.

Stack and heap memory

This reflects the memory usage of value and reference types. Value types have their instance data stored directly in the stack‘s frames while reference types place their instance data on the heap and are accessed via stack-stored references. The stack is fast and simple for memory allocation but limited in size, suiting the lightweight needs of value types, whereas the more dynamic but slower heap better accommodates reference types.

Another distinction between the siblings is their outlook on change. Value Type is quite rigid, always opting to make copies of himself whenever he needs to be in two places at once or transform in some way. He reasons that it‘s safer to create a whole separate version of himself rather than risk affecting the original.

let int1 = 42
var int2 = int1
int2 = 88

print(int1) // Output: 42
print(int2) // Output: 88

Despite being a value type, Int follows the same rules. Assigning int1 to int2 copies the value 42, and then changing int2 to 88 has no effect on int1.

Reference Type is more flexible and willing to expose himself to modification as he sees change as an inevitable part of life. He‘s comfortable with the idea that he may evolve over time and that his state from one day to the next isn‘t necessarily constant.

class Person {
var name: String
var age: Int

init(name: String, age: Int) {
self.name = name
self.age = age
}
}

let person1 = Person(name: "Alice", age: 25)
var person2 = person1
person2.age = 26

print(person1.age) // Output: 26
print(person2.age) // Output: 26

Here, person1 and person2 are two references to the same instance of the Person class. Changing the age via person2 is reflected when accessing it from person1. The reference semantics allow multiple ways to access and manipulate the same instance.

Comparing the Siblings

In school, Value Type and Reference Type each have their own way of making friends. Value Type believes in quality over quantity, only considering someone a true friend if they have the exact same values and interests as him. Two value type instances are considered equal if all their corresponding properties are equal.

struct Dog {
var name: String
var breed: String
var age: Int
}

let dog1 = Dog(name: "Buddy", breed: "Labrador", age: 3)
let dog2 = Dog(name: "Buddy", breed: "Labrador", age: 3)
let dog3 = Dog(name: "Daisy", breed: "Poodle", age: 6)

print(dog1 == dog2) // Output: true
print(dog1 == dog3) // Output: false

The Dog structs dog1 and dog2 are equal because all their stored properties are equal. But dog1 and dog3 differ in almost every property, so they are not considered equal. Value types use structural equality to compare instances.

Meanwhile, Reference Type is happy to welcome anyone as a friend, even if they don‘t have much in common. He believes relationships are built on shared experiences rather than initial similarities.

class Cat {
var name: String
var breed: String
var age: Int

init(name: String, breed: String, age: Int) {
self.name = name
self.breed = breed
self.age = age
}
}

let cat1 = Cat(name: "Whiskers", breed: "Siamese", age: 2)
let cat2 = Cat(name: "Whiskers", breed: "Siamese", age: 2)
let cat3 = cat1

print(cat1 === cat2) // Output: false
print(cat1 === cat3) // Output: true

Although cat1 and cat2 have all the same property values, they are separate instances and so not equal. However, cat1 and cat3 refer to the same instance, so they are equal. Reference types use referential equality – they are only equal if they refer to the exact same instance in memory.

Overcoming Challenges

As the brothers grew older and encountered more of life‘s obstacles, their unique traits allowed them to handle things differently. Value Type‘s independent nature shielded him from issues like race conditions and deadlocks that can occur when multiple people try to interact with a resource at the same time.

// Value type array of integers
var numbers = [1, 2, 3, 4, 5]

// Multiple threads accessing and mutating the array concurrently
DispatchQueue.concurrentPerform(iterations: 5) { index in
numbers[index] *= 10
}

print(numbers) // Output: [10, 20, 30, 40, 50]

Since numbers is a value type (Array), each thread gets its own independent copy of the array to modify safely without affecting the others. No unpredictable behavior occurs from concurrent access.

Value Type also never had to worry about complicated codependent relationships known as reference cycles where he and a friend couldn‘t survive without one another. His self-sufficient attitude meant he could easily detach from people as needed.

class Node {
var value: Int
var next: Node?

init(value: Int) {
self.value = value
}

deinit {
print("Node (value) is being deinitialized")
}
}

var node1: Node? = Node(value: 1)
var node2: Node? = Node(value: 2)

node1?.next = node2
node2?.next = node1

node1 = nil
node2 = nil // Neither node gets deinitialized

If node1 and node2 were value types, assigning them to nil would deallocate them immediately since there would be no lingering references. But as reference types, they point strongly to each other, preventing either from being destroyed until that cycle is broken. Value types are immune to this problem.

Reference Type sometimes felt jealous of his brother‘s simplicity, as he always needed to stay on top of his own memory management by counting references. It was like having to constantly check in with his friends to see who still wanted to hang out with him.

class Person {
var name: String

init(name: String) {
self.name = name
print("(name) is being initialized")
}

deinit {
print("(name) is being deinitialized")
}
}

var person1: Person? = Person(name: "Alice")
var person2 = person1
var person3 = person1

person1 = nil
// Alice is not deinitialized yet

person2 = nil
// Alice is not deinitialized yet

person3 = nil
// Alice is now deinitialized

Reference types use automatic reference counting (ARC) to track the active references to an instance. Only when all references are gone is the instance destroyed and its memory reclaimed. Value types have no such bookkeeping overhead.

Adapting to Change

As much as Value Type disliked change, he knew it was sometimes necessary to keep up with the demands of modern life. He developed a clever technique called copy-on-write which allowed him to share resources with his copies while things were static and only make a true independent copy in the event that one of them made a change. It was his way of balancing safety with efficiency.

var numbers1 = [1, 2, 3, 4, 5] var numbers2 = numbers1

numbers2.append(6)

print(numbers1) // Output: [1, 2, 3, 4, 5] print(numbers2) // Output: [1, 2, 3, 4, 5, 6]

numbers1 and numbers2 originally share the same array buffer. When an mutating operation like append happens on numbers2, it first copies the underlying buffer and then appends 6 to its own independent copy, leaving numbers1 unchanged. This optimization improves performance for large value types.

Finding Their Place

In the end, both brothers realized that they each had an important role to play in the Swift family. Value Type‘s stability and thread-safety made him well-suited for representing simple, independent concepts that could be easily copied without consequence. Currencies, coordinates, and colors are all good candidates to be modeled as value types.

struct Money {
var amount: Decimal
var currency: String
}

let usd = Money(amount: 100, currency: "USD")
let euro = usd
euro.currency = "EUR"

print(usd.currency) // Output: "USD"
print(euro.currency) // Output: "EUR"

Here, it makes sense for Money to be a value type because each instance should be a distinct, independent value. Copying an amount of money implicitly to make a currency conversion is a natural operation.

Reference Type‘s flexibility and shared mutability, on the other hand, aligned perfectly with more complex, stateful objects that needed to be accessed from multiple places. Entities in a database, UI components in an app, and nodes in a linked list are all inherently reference semantics.

class BankAccount {
var balance: Decimal

init(initialBalance: Decimal) {
balance = initialBalance
}

func deposit(_ amount: Decimal) {
balance += amount
}

func withdraw(_ amount: Decimal) {
balance -= amount
}
}

let account = BankAccount(initialBalance: 1000)
let statement1 = account
let statement2 = account

statement1.withdraw(100)
statement2.deposit(500)

print(account.balance) // Output: 1400

A BankAccount should be a reference type because many parts of a program may need to interact with the same account and see a consistent, updated balance. Treating it as a value type would create independent copies in each place, causing inconsistencies.

There is no one-size-fits-all rule for when to use value types or reference types. Each has its own strengths and tradeoffs that make it suitable for different scenarios. The decision ultimately comes down to the specific semantics your program requires. Value types are ideal for concepts that should be lightweight, independent values, while reference types excel at representing more complex objects with shared state and behavior.

So rather than pitting the two brothers against each other, it‘s best to recognize how they complement one another. Value Type and Reference Type may approach life differently, but together they make Swift a powerful and expressive language. By understanding their unique traits and the principles behind them, we can strive to design clear, cohesive programs that leverage their strengths appropriately.

The story of one mother and two sons may be a simple analogy, but it reflects the deeper truth that even within a unified Swift family, there is room for diversity in both structure and semantics. The key is to embrace and balance the differences, as Optionals balance the type system, and take advantage of Swift‘s flexibility to write code that is not only correct, but intuitive for both the compiler and the humans who will maintain it.

Similar Posts