Skip to main content

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:

swift
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 with let (constant), making the entire structure immutable
  • The mutablePerson is declared with var (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):

swift
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:

swift
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:

swift
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:

swift
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

swift
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

swift
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

  1. Create a BankAccount struct with properties for accountNumber (constant) and balance (variable). Add mutating methods for deposit, withdraw, and a non-mutating method checkBalance.

  2. 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).

  3. Implement a Stack<Element> struct with an array to store elements and methods to push, pop (mutating) and peek (non-mutating).

Additional Resources

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! :)