Swift Structure Mutability
Introduction
In Swift, structures (or structs) are value types that encapsulate related properties and behaviors. One important concept to understand when working with structures is mutability - the ability to change a struct's properties after it has been created.
Unlike classes, which are reference types, structs in Swift have special rules around mutability that may seem confusing at first. In this guide, we'll explore how mutability works with Swift structures, the difference between mutable and immutable struct instances, and how to use mutating methods.
Understanding Structure Mutability Basics
Value Types vs Reference Types
To understand struct mutability, we need to first remember that structs are value types:
- When you assign a struct to a variable, you're creating a complete copy of that struct
- Each copy is independent of the other copies
- Changing one copy does not affect other copies
This behavior is different from classes (reference types), where multiple variables can reference the same instance.
The Default: Immutability for Constants
Let's start with a basic example:
struct Person {
var name: String
var age: Int
}
let immutablePerson = Person(name: "John", age: 30)
var mutablePerson = Person(name: "Sarah", age: 28)
// Error: Cannot modify properties of a struct instance declared with 'let'
// immutablePerson.age = 31
// This works fine
mutablePerson.age = 29
print(mutablePerson.age) // Output: 29
In the example above:
- The
immutablePerson
is declared withlet
(constant), making the entire structure immutable - The
mutablePerson
is declared withvar
(variable), allowing us to modify its properties
Struct Properties and Mutability
Constant vs Variable Properties
Within a struct, you can declare properties as either constants (let
) or variables (var
):
struct Computer {
let serialNumber: String // Constant property
var ramGB: Int // Variable property
var storageGB: Int // Variable property
}
var myComputer = Computer(serialNumber: "MBP2023", ramGB: 16, storageGB: 512)
// We can modify variable properties
myComputer.ramGB = 32
myComputer.storageGB = 1024
// Error: Cannot assign to property - 'serialNumber' is a 'let' constant
// myComputer.serialNumber = "MBP2023-NEW"
print("Computer: \(myComputer.serialNumber), RAM: \(myComputer.ramGB)GB, Storage: \(myComputer.storageGB)GB")
// Output: Computer: MBP2023, RAM: 32GB, Storage: 1024GB
Even in a mutable struct instance (declared with var
), constant properties (let
) cannot be changed after initialization.
Mutating Methods
The Problem: Methods Changing Properties
In Swift, struct methods cannot modify the struct's properties by default. This is because when you call a method, Swift doesn't know if you might be calling it on a constant instance.
Let's see an example:
struct Counter {
var count: Int
// This will cause a compiler error
// func increment() {
// count += 1 // Error: Cannot assign to property
// }
}
The Solution: Mutating Methods
Swift provides the mutating
keyword to explicitly indicate that a method will modify the struct:
struct Counter {
var count: Int
// Using the 'mutating' keyword
mutating func increment() {
count += 1
}
mutating func reset() {
count = 0
}
// Non-mutating method (doesn't change properties)
func currentValue() -> Int {
return count
}
}
var myCounter = Counter(count: 0)
myCounter.increment()
myCounter.increment()
print(myCounter.currentValue()) // Output: 2
myCounter.reset()
print(myCounter.currentValue()) // Output: 0
// What happens with constants?
let constCounter = Counter(count: 10)
// constCounter.increment() // Error: Cannot use mutating method on immutable value
print(constCounter.currentValue()) // Output: 10 (this works because it's non-mutating)
How Mutating Methods Work Under the Hood
When you mark a method as mutating
, you're telling Swift that this method will replace the entire struct instance with a new one:
struct Point {
var x: Int
var y: Int
mutating func moveByX(_ deltaX: Int, y deltaY: Int) {
// Behind the scenes, this is essentially:
// self = Point(x: self.x + deltaX, y: self.y + deltaY)
x += deltaX
y += deltaY
}
// You can even replace 'self' entirely in a mutating method
mutating func reset() {
self = Point(x: 0, y: 0)
}
}
var myPoint = Point(x: 10, y: 20)
myPoint.moveByX(5, y: 10)
print("Point at: (\(myPoint.x), \(myPoint.y))") // Output: Point at: (15, 30)
myPoint.reset()
print("After reset: (\(myPoint.x), \(myPoint.y))") // Output: After reset: (0, 0)
Practical Examples
Example 1: Building a Temperature Converter
struct TemperatureConverter {
var celsius: Double
init(celsius: Double) {
self.celsius = celsius
}
// Computed properties don't need the mutating keyword
var fahrenheit: Double {
get {
return celsius * 9/5 + 32
}
set {
celsius = (newValue - 32) * 5/9
}
}
var kelvin: Double {
get {
return celsius + 273.15
}
set {
celsius = newValue - 273.15
}
}
mutating func setToBoilingPoint() {
celsius = 100
}
mutating func setToFreezingPoint() {
celsius = 0
}
}
var temp = TemperatureConverter(celsius: 25)
print("Celsius: \(temp.celsius)°C") // Output: Celsius: 25.0°C
print("Fahrenheit: \(temp.fahrenheit)°F") // Output: Fahrenheit: 77.0°F
print("Kelvin: \(temp.kelvin)K") // Output: Kelvin: 298.15K
// Modify using the fahrenheit property
temp.fahrenheit = 32
print("After setting to 32°F: \(temp.celsius)°C") // Output: After setting to 32°F: 0.0°C
// Use a mutating method
temp.setToBoilingPoint()
print("After boiling: \(temp.celsius)°C, \(temp.fahrenheit)°F") // Output: After boiling: 100.0°C, 212.0°F
Example 2: Building a Shopping Cart
struct Item {
let id: Int
let name: String
let price: Double
}
struct ShoppingCart {
private var items: [Item] = []
// Read-only property
var totalPrice: Double {
return items.reduce(0) { $0 + $1.price }
}
var itemCount: Int {
return items.count
}
// Mutating methods
mutating func addItem(_ item: Item) {
items.append(item)
}
mutating func removeItem(id: Int) {
if let index = items.firstIndex(where: { $0.id == id }) {
items.remove(at: index)
}
}
mutating func clearCart() {
items = []
}
// Non-mutating method
func containsItem(id: Int) -> Bool {
return items.contains { $0.id == id }
}
// Non-mutating method
func listItems() -> [String] {
return items.map { "\($0.name): $\($0.price)" }
}
}
// Create some items
let laptop = Item(id: 1, name: "Laptop", price: 1299.99)
let headphones = Item(id: 2, name: "Headphones", price: 199.99)
let charger = Item(id: 3, name: "Charger", price: 49.99)
// Create a shopping cart
var cart = ShoppingCart()
cart.addItem(laptop)
cart.addItem(headphones)
print("Items in cart: \(cart.itemCount)") // Output: Items in cart: 2
print("Total: $\(cart.totalPrice)") // Output: Total: $1499.98
// List all items
cart.listItems().forEach { print($0) }
// Output:
// Laptop: $1299.99
// Headphones: $199.99
// Check if an item exists
print("Cart contains charger: \(cart.containsItem(id: 3))") // Output: Cart contains charger: false
// Remove an item
cart.removeItem(id: 2)
print("After removing headphones, total: $\(cart.totalPrice)") // Output: After removing headphones, total: $1299.99
// Clear cart
cart.clearCart()
print("After clearing, items in cart: \(cart.itemCount)") // Output: After clearing, items in cart: 0
Common Pitfalls and Best Practices
1. Forgetting the mutating
Keyword
One of the most common errors is forgetting to mark methods that modify properties as mutating
. The compiler will catch this for you, but it's good to get in the habit of considering whether a method will modify the structure.
2. Using Mutating Methods on Constants
Remember that you cannot call mutating methods on struct instances that are declared with let
. This will result in a compiler error.
3. When to Use Mutating Methods
A general guideline:
- Use non-mutating methods when you need to retrieve information without changing state
- Use mutating methods when you need to update the struct's internal state
- Consider using computed properties for derived values
4. Mutable vs Immutable Design
When designing your structures, think about which properties should be mutable and which should be immutable. Immutable properties (let
) add safety by ensuring that values cannot change after initialization.
Summary
In Swift, structure mutability is a powerful concept that helps maintain data integrity and predictability. Here's what we've covered:
- Structures are value types, creating independent copies when assigned
- Struct instances declared with
let
are completely immutable - Struct instances declared with
var
allow modification of variable properties - Methods that modify struct properties must be marked with the
mutating
keyword - You cannot call mutating methods on immutable instances (declared with
let
) - Mutating methods conceptually replace the entire struct instance
Understanding struct mutability helps you write safer, more predictable code by controlling when and how your data can change.
Further Learning
Exercises
-
Create a
BankAccount
struct with properties foraccountNumber
(constant) andbalance
(variable). Add mutating methods fordeposit
,withdraw
, and a non-mutating methodcheckBalance
. -
Build a
Rectangle
struct with width and height properties. Add methods to calculate area and perimeter (non-mutating) and methods to scale the rectangle dimensions by a factor (mutating). -
Implement a
Stack<Element>
struct with an array to store elements and methods topush
,pop
(mutating) andpeek
(non-mutating).
Additional Resources
- Swift Documentation on Properties
- Swift Documentation on Methods
- Value Semantics in Swift
- WWDC Sessions on Swift Value Types
By mastering struct mutability in Swift, you'll be well-equipped to create robust and predictable code that follows Swift's best practices for value types.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)