Swift Classes and Structs — Value Types vs Reference Types Explained
In this tutorial, you will learn about Swift Classes and Structs. We cover key concepts, practical examples, and best practices to help you master this topic.
Swift classes and structs are the building blocks of object-oriented code, with structs using value semantics (copied on assignment) and classes using reference semantics (shared by reference) with automatic reference counting for memory management. This tutorial covers defining structs and classes with properties and methods, understanding the difference between value and reference types, ARC memory management, inheritance for classes, mutability rules for structs, and guidelines for choosing between them.
What You'll Learn
- Defining structs with properties, methods, and initializers
- Defining classes with reference semantics and inheritance
- Understanding value vs reference type behavior
- How ARC manages memory for class instances
- Using memberwise initializers for structs
- Mutability rules: let with struct vs let with class
- Choosing between struct and class for different use cases
- Using identity operators === and !==
Why It Matters
Choosing the right type is one of the most important decisions in Swift design. Using a class where a struct suffices introduces reference semantics and ARC overhead unnecessarily. Using a struct where identity matters (like a database object) causes confusion when copies diverge. Understanding this distinction is essential for writing idiomatic Swift code.
Real-World Use
Doda Browser uses structs for all data models (URL metadata, bookmarks, history entries) and classes only for services (network manager, database manager, session manager). This separation ensures that data is passed safely between threads without unexpected mutations, while services that inherently have identity use reference semantics.
Learning Path
flowchart LR A[Error Handling] --> B[Classes and Structs\nYou are here] B --> C[Initializers] style B fill:#f90,color:#fff
Defining Structs
Structs are value types with automatic memberwise initializers:
import Foundation
// Defining a struct
struct Book {
var title: String
var author: String
var pages: Int
var isRead: Bool
// Computed property
var readingTime: Int {
return pages / 30 // Assuming 30 pages per hour
}
// Method
func description() -> String {
return "\"\(title)\" by \(author) (\(pages) pages)"
}
// Mutating method (required for value types)
mutating func markAsRead() {
isRead = true
}
}
// Memberwise initializer (automatic)
var book = Book(title: "1984", author: "George Orwell", pages: 328, isRead: false)
print(book.description())
print("Reading time: ~\(book.readingTime) hours")
book.markAsRead()
print("Read: \(book.isRead)")
Output:
"1984" by George Orwell (328 pages)
Reading time: ~10 hours
Read: true
Defining Classes
Classes are reference types with inheritance:
import Foundation
class Vehicle {
var make: String
var model: String
var year: Int
var mileage: Double
init(make: String, model: String, year: Int, mileage: Double = 0) {
self.make = make
self.model = model
self.year = year
self.mileage = mileage
}
func description() -> String {
return "\(year) \(make) \(model) (\(Int(mileage)) miles)"
}
func drive(miles: Double) {
mileage += miles
}
}
// Inheritance
class ElectricCar: Vehicle {
var batteryCapacity: Double // kWh
var chargeLevel: Double = 1.0 // 0.0 to 1.0
init(make: String, model: String, year: Int, batteryCapacity: Double) {
self.batteryCapacity = batteryCapacity
super.init(make: make, model: model, year: year)
}
override func description() -> String {
return "\(super.description()) - Electric, \(Int(batteryCapacity)) kWh"
}
func charge(to level: Double) {
chargeLevel = min(1.0, max(0.0, level))
}
}
let tesla = ElectricCar(make: "Tesla", model: "Model 3", year: 2024, batteryCapacity: 75)
tesla.drive(miles: 100)
tesla.charge(to: 0.8)
print(tesla.description())
print("Battery: \(Int(tesla.chargeLevel * 100))%")
Output:
2024 Tesla Model 3 (100 miles) - Electric, 75 kWh
Battery: 80%
Value vs Reference Semantics
The key difference between structs and classes:
import Foundation
// Struct: value type - copied on assignment
struct PointStruct {
var x: Double
var y: Double
}
// Class: reference type - shared reference
class PointClass {
var x: Double
var y: Double
init(x: Double, y: Double) {
self.x = x
self.y = y
}
}
// Struct behavior - independent copies
var point1 = PointStruct(x: 10, y: 20)
var point2 = point1
point2.x = 100
print("Struct: point1.x = \(point1.x), point2.x = \(point2.x)")
// Class behavior - shared reference
var point3 = PointClass(x: 10, y: 20)
var point4 = point3
point4.x = 100
print("Class: point3.x = \(point3.x), point4.x = \(point4.x)")
Output:
Struct: point1.x = 10.0, point2.x = 100.0
Class: point3.x = 100.0, point4.x = 100.0
ARC Memory Management
Automatic Reference Counting manages class instance lifetimes:
import Foundation
class Logger {
let id: Int
init(id: Int) { self.id = id; print("Logger \(id) initialized") }
deinit { print("Logger \(id) deinitialized") }
}
func createLoggers() {
var logger1: Logger? = Logger(id: 1) // ref count: 1
var logger2 = logger1 // ref count: 2
print("Both references alive")
logger1 = nil // ref count: 1
print("logger1 set to nil")
logger2 = nil // ref count: 0 -> deinit
print("logger2 set to nil")
}
createLoggers()
Output:
Logger 1 initialized
Both references alive
logger1 set to nil
Logger 1 deinitialized
logger2 set to nil
Identity Operators
Use === to check if two class references point to the same instance:
import Foundation
class Widget {
var name: String
init(name: String) { self.name = name }
}
let widget1 = Widget(name: "Button")
let widget2 = Widget(name: "Button")
let widget3 = widget1
print("widget1 === widget2: \(widget1 === widget2)")
print("widget1 === widget3: \(widget1 === widget3)")
print("widget1 == widget2 (value): \(widget1.name == widget2.name)")
Output:
widget1 === widget2: false
widget1 === widget3: true
widget1 == widget2 (value): true
Choosing Between Struct and Class
| Use Struct When | Use Class When |
|---|---|
| Data is a value (coordinates, money) | Identity matters (database objects) |
| No inheritance needed | Need inheritance or subtyping |
| Data is small and frequently copied | Instance has side effects (file handles) |
| Mutability is used sparingly | Reference counting overhead is acceptable |
| Type should be thread-safe | Integration with Objective-C required |
Common Mistakes
- Using a class for a data model: Most data types (User, Product, Point) should be structs. Classes introduce reference semantics and ARC overhead unnecessarily.
- Mutating a struct property through a let reference:
let rect = CGRect(...); rect.origin.x = 5fails because the struct is immutable. Use var. - Creating retain cycles: Two class instances holding strong references to each other creates a leak. Use weak references for delegate patterns.
- Assuming class instances are thread-safe: Reference types are not automatically thread-safe. Shared mutable state requires synchronization.
- Overriding deinit without calling super: Classes that override deinit must call super.deinit for proper cleanup.
Practice Questions
What is the difference between a value type and a reference type?
- Value types (structs, enums) are copied on assignment. Reference types (classes) share a single instance. Mutations to one reference affect all references.
When does a struct memberwise initializer exist?
- Automatically when no custom initializers are defined. It initializes all stored properties in declaration order.
What is a retain cycle and how do you prevent it?
- A retain cycle occurs when two class instances hold strong references to each other. Break the cycle with weak or unowned references.
What does the
mutatingkeyword do in a struct method?- It marks a method that modifies stored properties. Struct methods cannot mutate properties without
mutating.
- It marks a method that modifies stored properties. Struct methods cannot mutate properties without
Challenge: Refactor a class-based data model to use structs. Identify which properties need mutation and add mutating methods. Compare the memory usage and Thread Safety of both approaches.
Mini Project
Build a geometric shape library:
- Define a
Shapeprotocol with area() and perimeter() methods. - Implement structs: Circle, Rectangle, Triangle that conform to Shape.
- Implement a Canvas class that holds an array of Shape and can render descriptions.
- Demonstrate value type independence (modifying a Circle copy does not affect the original).
- Demonstrate reference type sharing (Canvas is shared between two view controllers).
FAQ
What's Next
Learn how to initialize types properly in the Initializers tutorial.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro