Swift Structures vs Classes
Introduction
In Swift, both structures (struct
) and classes (class
) are fundamental building blocks that allow you to create custom data types. While they share many similarities, their underlying behavior and use cases differ significantly. Understanding when to use each is crucial for writing efficient, maintainable Swift code.
This guide explores the key differences between structures and classes, helping you make informed decisions in your Swift programming journey.
Key Differences at a Glance
Before diving into detailed explanations, here's a quick overview of the main differences:
Feature | Structures | Classes |
---|---|---|
Type | Value type | Reference type |
Inheritance | No inheritance | Supports inheritance |
Initialization | Auto-generated memberwise initializers | No default memberwise initializers |
Memory Management | Automatically handled | Uses ARC (Automatic Reference Counting) |
Mutability | Requires mutating keyword for methods that change properties | No special keyword required |
Identity operators | Cannot use === and !== | Can use identity operators |
Value Types vs Reference Types
The most fundamental difference between structures and classes is how they're handled in memory.
Structures: Value Types
When you assign a structure to a variable or pass it to a function, Swift creates a copy of the structure.
struct Point {
var x: Int
var y: Int
}
var point1 = Point(x: 10, y: 20)
var point2 = point1 // Creates a copy
point2.x = 30
print("Point 1: (\(point1.x), \(point1.y))") // Output: Point 1: (10, 20)
print("Point 2: (\(point2.x), \(point2.y))") // Output: Point 2: (30, 20)
As you can see, changing point2
doesn't affect point1
because point2
is a completely separate copy.
Classes: Reference Types
When you assign a class instance to a variable or pass it to a function, Swift passes a reference to the same instance, not a copy.
class Person {
var name: String
init(name: String) {
self.name = name
}
}
var person1 = Person(name: "John")
var person2 = person1 // Both variables now refer to the same instance
person2.name = "Jane"
print("Person 1: \(person1.name)") // Output: Person 1: Jane
print("Person 2: \(person2.name)") // Output: Person 2: Jane
Changing person2
also changes person1
because both variables reference the same instance in memory.
Inheritance
Classes Support Inheritance
Classes can inherit properties, methods, and other characteristics from another class:
class Vehicle {
var currentSpeed = 0.0
func description() -> String {
return "traveling at \(currentSpeed) miles per hour"
}
}
class Bicycle: Vehicle {
var hasBasket = false
override func description() -> String {
return "a bicycle \(super.description())"
}
}
let bike = Bicycle()
bike.currentSpeed = 15.0
print(bike.description()) // Output: a bicycle traveling at 15.0 miles per hour
Structures Don't Support Inheritance
Structures cannot inherit from other structures. However, they can adopt protocols, which offers a different way to share functionality:
protocol Movable {
var speed: Double { get set }
func move()
}
struct Car: Movable {
var speed: Double = 0.0
func move() {
print("Car is moving at \(speed) mph")
}
}
var myCar = Car()
myCar.speed = 55.0
myCar.move() // Output: Car is moving at 55.0 mph
Initialization Differences
Structures: Auto-generated Memberwise Initializers
Swift automatically provides memberwise initializers for structures:
struct Rectangle {
var width: Double
var height: Double
}
// Swift automatically creates this initializer
let rectangle = Rectangle(width: 10.0, height: 5.0)
Classes: No Default Memberwise Initializers
Classes don't receive automatic memberwise initializers. You must provide your own initializers:
class Rectangle {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
}
let rectangle = Rectangle(width: 10.0, height: 5.0)
Mutability and the mutating
Keyword
Structures Require mutating
Keyword
When you need to modify a structure's property within a method, you must mark the method as mutating
:
struct Counter {
var count = 0
// This requires 'mutating' because it changes a property
mutating func increment() {
count += 1
}
// This doesn't need 'mutating' because it doesn't change any properties
func currentCount() -> Int {
return count
}
}
var counter = Counter()
counter.increment()
print(counter.currentCount()) // Output: 1
Classes Don't Require Special Handling
Classes don't need special handling for methods that modify properties:
class Counter {
var count = 0
// No 'mutating' keyword needed
func increment() {
count += 1
}
func currentCount() -> Int {
return count
}
}
let counter = Counter()
counter.increment()
print(counter.currentCount()) // Output: 1
Note: You can modify the properties of a class instance even if the instance is stored in a constant (let
) variable, because the reference remains constant but the content can change.
Identity Operators
Classes support identity operators (===
and !==
), which check if two references point to the exact same instance:
class Person {
var name: String
init(name: String) {
self.name = name
}
}
let person1 = Person(name: "John")
let person2 = Person(name: "John")
let person3 = person1
print(person1 === person2) // Output: false (different instances)
print(person1 === person3) // Output: true (same instance)
Structures don't have identity operators because each structure instance is a unique copy with its own identity.
Memory Management
Classes Use ARC (Automatic Reference Counting)
Swift uses ARC to track and manage class instances. This can lead to concerns like reference cycles:
class Person {
var name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class Apartment {
var unit: String
weak var tenant: Person? // Using 'weak' to avoid strong reference cycle
init(unit: String) {
self.unit = unit
}
deinit {
print("Apartment \(unit) is being deinitialized")
}
}
// Creating a scope to demonstrate memory management
do {
let john = Person(name: "John")
let apartment = Apartment(unit: "4A")
john.apartment = apartment
apartment.tenant = john
} // Both objects will be deinitialized when the scope ends
// Output:
// John is being deinitialized
// Apartment 4A is being deinitialized
Structures Have Simpler Memory Management
Since structures are value types, they're automatically deallocated when they go out of scope, with no need for reference counting.
When to Use Structures vs Classes
Use Structures When:
- You need a simple data type to represent a value
- You want copies to be unique and separate
- The data will be used in a multi-threaded environment
- You want to avoid inheritance
- The data is relatively small in size
Real-world examples:
- Geometric shapes (Point, Size, Rect)
- Mathematical values (Vector, Matrix)
- Value-based models (Currency, Temperature)
struct Temperature {
var celsius: Double
var fahrenheit: Double {
return celsius * 9/5 + 32
}
var kelvin: Double {
return celsius + 273.15
}
}
var currentTemp = Temperature(celsius: 25)
print("It's \(currentTemp.celsius)°C or \(currentTemp.fahrenheit)°F today.")
// Output: It's 25.0°C or 77.0°F today.
Use Classes When:
- You need to control identity (reference semantics)
- You need inheritance
- You need to use deinitializers
- You're working with Objective-C interoperability
- You're modeling a complex system with shared state
Real-world examples:
- UIView and its subclasses in UIKit
- File handles or network connections
- Complex data models that need inheritance
class BankAccount {
let accountNumber: String
var balance: Double
let owner: String
init(accountNumber: String, initialBalance: Double, owner: String) {
self.accountNumber = accountNumber
self.balance = initialBalance
self.owner = owner
}
func deposit(amount: Double) {
balance += amount
}
func withdraw(amount: Double) -> Bool {
guard balance >= amount else {
return false
}
balance -= amount
return true
}
deinit {
print("Account \(accountNumber) is being closed")
}
}
// Bank system example - multiple references to the same account
let johnsAccount = BankAccount(accountNumber: "12345", initialBalance: 1000, owner: "John")
// Bank teller and ATM both reference the same account
let tellerAccess = johnsAccount
let atmAccess = johnsAccount
tellerAccess.deposit(amount: 500)
print(atmAccess.balance) // Output: 1500.0 - reflected across all references
Practical Example: Building an App
Let's examine how structures and classes might be used together in a simple note-taking app:
// Note content is a value type - perfect for a struct
struct NoteContent {
var title: String
var text: String
var creationDate: Date
var lastModified: Date
mutating func update(newText: String) {
text = newText
lastModified = Date()
}
}
// NotesManager handles operations and maintains state - better as a class
class NotesManager {
var notes: [NoteContent] = []
func createNote(title: String, text: String) {
let now = Date()
let newNote = NoteContent(title: title, text: text, creationDate: now, lastModified: now)
notes.append(newNote)
}
func deleteNote(at index: Int) {
guard index < notes.count else { return }
notes.remove(at: index)
}
func updateNote(at index: Int, newText: String) {
guard index < notes.count else { return }
var note = notes[index]
note.update(newText: newText)
notes[index] = note
}
func getAllNotes() -> [NoteContent] {
return notes
}
}
// Usage example
let manager = NotesManager()
manager.createNote(title: "Shopping List", text: "Milk, Eggs, Bread")
manager.createNote(title: "Task List", text: "Learn Swift, Build App")
// Update a note
manager.updateNote(at: 0, newText: "Milk, Eggs, Bread, Cheese")
// Display all notes
for (index, note) in manager.getAllNotes().enumerated() {
print("Note \(index + 1): \(note.title)")
print("Content: \(note.text)")
print("Last modified: \(note.lastModified)")
print("------------------")
}
In this example:
NoteContent
is a structure because it represents a value (the content of a note)NotesManager
is a class because it maintains state and handles operations on the notes collection
Performance Considerations
Understanding the performance implications can help you make better decisions:
-
Structures generally have better performance for small to medium-sized data because:
- Allocation is often faster (stack vs heap)
- No reference counting overhead
- Better memory locality
-
Classes can be more efficient when:
- Objects are large and would be expensive to copy
- Objects are frequently passed around but rarely modified
- Shared state is needed
Summary
Understanding the differences between structures and classes is fundamental to writing effective Swift code:
-
Structures are value types that create copies when assigned or passed. They're great for representing simple data that stands on its own and doesn't need inheritance.
-
Classes are reference types that share a single instance when assigned or passed. They support inheritance and are ideal for complex objects that maintain state or need identity.
Apple's recommendation is to use structures by default and switch to classes when you specifically need reference semantics, inheritance, or other class features.
Practice Exercises
- Create a structure to represent a playing card with suit and rank properties.
- Create a class to represent a deck of cards that can shuffle and deal cards.
- Experiment with a
Person
struct vs aPerson
class and observe the different behaviors when you copy instances. - Create a simple game that uses both structures and classes appropriately.
Additional Resources
- Swift Programming Language Guide: Classes and Structures
- WWDC 2015: Building Better Apps with Value Types in Swift
- Swift by Sundell: Reference and Value Types in Swift
- Apple Developer Documentation: Choosing Between Structures and Classes
Happy coding with Swift structures and classes!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)