Classes and Structs
Classes vs Structs
Section titled “Classes vs Structs”Swift provides both classes (reference types) and structs (value types). The choice between them is a fundamental design decision.
| Feature | Struct | Class |
|---|---|---|
| Type | Value type | Reference type |
| Assignment | Copied | Shared reference |
| Inheritance | No | Yes |
| Deinitialiser | No | Yes (deinit) |
| Mutability | Must use var + mutating | Properties always mutable |
| Memory | Stack (for small structs) | Heap (ARC) |
| Identity | No (== compares values) | Yes (=== compares references) |
Implicit init | Yes (memberwise) | No |
Choosing Between Struct and Class
Section titled “Choosing Between Struct and Class”Use structs by default. Switch to classes when you need:
- Inheritance
- Shared mutable state (identity semantics)
- Objective-C interoperability
- Deinitialisation (
deinit)
// Struct -- value typestruct Point { var x: Double var y: Double}
var p1 = Point(x: 1.0, y: 2.0)var p2 = p1 // Copyp2.x = 10.0print(p1.x) // 1.0 (unchanged)
// Class -- reference typeclass Dog { var name: String init(name: String) { self.name = name }}
var d1 = Dog(name: "Rex")var d2 = d1 // Same referenced2.name = "Buddy"print(d1.name) // Buddy (changed)print(d1 === d2) // true (same object)Properties
Section titled “Properties”Stored Properties
Section titled “Stored Properties”struct Rectangle { var width: Double var height: Double
// Lazy stored property -- initialised on first access lazy var area: Double = width * height}
var rect = Rectangle(width: 10, height: 5)print(rect.area) // 50.0 (computed now)Computed Properties
Section titled “Computed Properties”struct Circle { var radius: Double
// Read-only computed property var diameter: Double { radius * 2 }
// Read-write computed property var circumference: Double { get { 2 * .pi * radius } set { radius = newValue / (2 * .pi) } }
// Computed property with observer is NOT allowed}
var circle = Circle(radius: 5)print(circle.circumference) // 31.4159...circle.circumference = 62.83print(circle.radius) // 10.0Property Observers
Section titled “Property Observers”class StepCounter { var totalSteps: Int = 0 { willSet { print("About to set to \(newValue)") } didSet { print("Changed from \(oldValue) to \(totalSteps)") if totalSteps > 10000 { print("Goal reached!") } } }}
let counter = StepCounter()counter.totalSteps = 200// About to set to 200// Changed from 0 to 200
counter.totalSteps = 10500// About to set to 10500// Changed from 200 to 10500// Goal reached!Type Properties
Section titled “Type Properties”struct Configuration { static let apiVersion = "v2" static var requestCount = 0
static func reset() { requestCount = 0 }
class var description: String { "Configuration \(apiVersion)" }}Methods
Section titled “Methods”Instance Methods
Section titled “Instance Methods”class Counter { var count = 0
func increment() { count += 1 } func increment(by amount: Int) { count += amount } func reset() { count = 0 }}Mutating Methods (Structs Only)
Section titled “Mutating Methods (Structs Only)”struct Point { var x: Double var y: Double
mutating func moveBy(dx: Double, dy: Double) { x += dx y += dy }
mutating func reset() { self = Point(x: 0, y: 0) }}
var origin = Point(x: 0, y: 0)origin.moveBy(dx: 3, dy: 4)Type Methods
Section titled “Type Methods”struct MathHelpers { static func factorial(_ n: Int) -> Int { guard n > 0 else { return 1 } return n * factorial(n - 1) }
static func isPrime(_ n: Int) -> Bool { guard n > 1 else { return false } for i in 2..<n where i * i <= n { if n % i == 0 { return false } } return true }}Initialisation
Section titled “Initialisation”Memberwise Initialiser (Structs)
Section titled “Memberwise Initialiser (Structs)”struct Person { let name: String var age: Int}
// Auto-generated: Person(name:age:)let alice = Person(name: "Alice", age: 30)Custom Initialisers
Section titled “Custom Initialisers”class Temperature { var celsius: Double
init(celsius: Double) { self.celsius = celsius }
init(fahrenheit: Double) { self.celsius = (fahrenheit - 32) * 5 / 9 }
init(kelvin: Double) { self.celsius = kelvin - 273.15 }
convenience init(fromString s: String) { if s.hasSuffix("F") { let val = Double(s.dropLast()) ?? 0 self.init(fahrenheit: val) } else { let val = Double(s) ?? 0 self.init(celsius: val) } }}Required and Failable Initialisers
Section titled “Required and Failable Initialisers”class Animal { let species: String
required init(species: String) { self.species = species }
// Failable initialiser convenience init?(species: String?) { guard let species, !species.isEmpty else { return nil } self.init(species: species) }}
class Dog: Animal { let breed: String
init(breed: String) { self.breed = breed super.init(species: "Dog") }
// Must implement required initialiser required init(species: String) { self.breed = "Mixed" super.init(species: species) }}Deinitialisation
Section titled “Deinitialisation”class FileManager { let filename: String
init(filename: String) { self.filename = filename print("Opened \(filename)") }
deinit { print("Closed \(filename)") }}
if true { let fm = FileManager(filename: "data.txt") print("Using file...")}// "Closed data.txt" printed automatically when fm goes out of scopeInheritance
Section titled “Inheritance”class Vehicle { var speed: Double = 0 let make: String
init(make: String) { self.make = make }
func describe() -> String { "\(make) moving at \(speed) km/h" }
// Prevent override final func typeName() -> String { "Vehicle" }}
class Car: Vehicle { var numberOfDoors: Int
init(make: String, doors: Int) { self.numberOfDoors = doors super.init(make: make) }
override func describe() -> String { return "\(make) (\(numberOfDoors)-door) at \(speed) km/h" }}
class ElectricCar: Car { var batteryLevel: Double = 100
override func describe() -> String { return "\(make) EV at \(speed) km/h (battery: \(batteryLevel)%)" }}
let tesla = ElectricCar(make: "Tesla", doors: 4)tesla.speed = 80print(tesla.describe()) // Tesla EV at 80 km/h (battery: 100%)Overriding Properties
Section titled “Overriding Properties”class Shape { var color: String = "black"}
class ColoredShape: Shape { override var color: String { didSet { print("Color changed to \(color)") } }}Protocols
Section titled “Protocols”Protocols define a blueprint of methods, properties, and requirements that conforming types must implement. They are central to Swift”s protocol-oriented programming paradigm.
Defining Protocols
Section titled “Defining Protocols”protocol Drawable { func draw()}
protocol Resizable { var scale: Double { get set } func resize(by factor: Double)}
protocol Identifiable { var id: UUID { get } var name: String { get }}
protocol Configurable { static var defaultConfiguration: Self { get } init(configuration: Self)}Protocol Conformance
Section titled “Protocol Conformance”protocol ShapeProtocol { var area: Double { get } func describe() -> String}
struct Circle: ShapeProtocol { var radius: Double
var area: Double { .pi * radius * radius }
func describe() -> String { return "Circle (r=\(radius), area=\(area))" }}
struct Rectangle: ShapeProtocol { var width: Double var height: Double
var area: Double { width * height }
func describe() -> String { return "Rectangle (\(width)x\(height), area=\(area))" }}
// Polymorphism through protocolslet shapes: [ShapeProtocol] = [Circle(radius: 5), Rectangle(width: 4, height: 6)]for shape in shapes { print(shape.describe())}Protocol Extensions with Default Implementations
Section titled “Protocol Extensions with Default Implementations”protocol Loggable { var logIdentifier: String { get } func log(_ message: String)}
extension Loggable { func log(_ message: String) { print("[\(logIdentifier)] \(message)") }}
// Now any type conforming to Loggable gets log() for freestruct UserService: Loggable { var logIdentifier: String { "UserService" }}
let service = UserService()service.log("User logged in") // [UserService] User logged inProtocol Composition
Section titled “Protocol Composition”protocol Named { var name: String { get }}
protocol Aged { var age: Int { get }}
protocol Employee: Named, Aged { var department: String { get }}
func greet(_ person: some Named & Aged) { print("Hello, \(person.name). You are \(person.age) years old.")}Existential Types (any and some)
Section titled “Existential Types (any and some)”// any -- existential (type-erased) containerfunc draw(_ shape: any ShapeProtocol) { shape.draw()}
// some -- opaque type (caller doesn't know the concrete type)func makeShape() -> some ShapeProtocol { return Circle(radius: 5) // Concrete type hidden from caller}Extensions
Section titled “Extensions”Extensions add new functionality to existing types without subclassing.
extension Double { var isInteger: Bool { self == rounded() } var squared: Double { self * self } func clamped(to range: ClosedRange<Double>) -> Double { return min(max(self, range.lowerBound), range.upperBound) }}
let value = 3.7print(value.isInteger) // falseprint(value.squared) // 13.69print(value.clamped(to: 0...3)) // 3.0
// Extension on Stringextension String { var isEmail: Bool { return self.contains("@") && self.contains(".") }
func masked() -> String { guard count > 2 else { return self } return String(self.prefix(2)) + String(repeating: "*", count: count - 2) }}
print("hello@example.com".isEmail) // trueprint("secret".masked()) // "se****"Extensions with Protocol Conformance
Section titled “Extensions with Protocol Conformance”extension Int: Loggable { var logIdentifier: String { "Int(\(self))" }}
5.log("Logging from an integer")Generics
Section titled “Generics”Generics write flexible, reusable code that works with any type while maintaining type safety.
Generic Functions
Section titled “Generic Functions”func swapValues<T>(_ a: inout T, _ b: inout T) { let temp = a a = b b = temp}
var x = "hello", y = "world"swapValues(&x, &y)
func firstElement<T>(of array: [T]) -> T? { return array.first}
func identical<T: Equatable>(_ a: T, _ b: T) -> Bool { return a == b}Generic Types
Section titled “Generic Types”struct Stack<Element> { private var elements: [Element] = []
var isEmpty: Bool { elements.isEmpty } var top: Element? { elements.last } var count: Int { elements.count }
mutating func push(_ element: Element) { elements.append(element) }
mutating func pop() -> Element? { return elements.popLast() }}
var intStack = Stack<Int>()intStack.push(1)intStack.push(2)print(intStack.pop()) // 2
var stringStack = Stack<String>()stringStack.push("hello")print(stringStack.top) // Optional("hello")Generic Constraints
Section titled “Generic Constraints”func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? { for (index, element) in array.enumerated() { if element == value { return index } } return nil}
// Multiple constraintsprotocol Container { associatedtype Item var count: Int { get } subscript(i: Int) -> Item { get }}
func allItemsMatch<C1: Container, C2: Container>( _ c1: C1, _ c2: C2) -> Bool where C1.Item: Equatable, C1.Item == C2.Item { guard c1.count == c2.count else { return false } for i in 0..<c1.count { if c1[i] != c2[i] { return false } } return true}Associated Types
Section titled “Associated Types”protocol IteratorProtocol { associatedtype Element mutating func next() -> Element?}
struct CountdownIterator: IteratorProtocol { typealias Element = Int var current: Int
mutating func next() -> Int? { guard current > 0 else { return nil } current -= 1 return current + 1 }}
// some IteratorProtocolfunc makeIterator() -> some IteratorProtocol { return CountdownIterator(current: 3)}Memory Management — ARC
Section titled “Memory Management — ARC”Automatic Reference Counting (ARC) automatically manages memory for reference types.
Strong References
Section titled “Strong References”class Person { let name: String init(name: String) { self.name = name } deinit { print("\(name) is being deallocated") }}
var reference1: Person? = Person(name: "Alice")// reference1 -> Alice (reference count: 1)var reference2 = reference1// reference count: 2reference1 = nil// reference count: 1 (still alive)reference2 = nil// reference count: 0 -> deallocated, deinit printsStrong Reference Cycles
Section titled “Strong Reference Cycles”class Person2 { let name: String var apartment: Apartment? init(name: String) { self.name = name } deinit { print("\(name) deallocated") }}
class Apartment { let unit: String var tenant: Person2? init(unit: String) { self.unit = unit } deinit { print("Apartment \(unit) deallocated") }}
// This creates a strong reference cyclevar john: Person2? = Person2(name: "John")var unit4A: Apartment? = Apartment(unit: "4A")john?.apartment = unit4Aunit4A?.tenant = johnjohn = nilunit4A = nil// Neither is deallocated -- memory leak!Resolving Cycles with weak and unowned
Section titled “Resolving Cycles with weak and unowned”class Person3 { let name: String weak var apartment: Apartment2? init(name: String) { self.name = name } deinit { print("\(name) deallocated") }}
class Apartment2 { let unit: String unowned let tenant: Person3 init(unit: String, tenant: Person3) { self.unit = unit; self.tenant = tenant } deinit { print("Apartment \(unit) deallocated") }}
var john2: Person3? = Person3(name: "John")var unit4B: Apartment2? = Apartment2(unit: "4B", tenant: john2!)john2?.apartment = unit4Bjohn2 = nilunit4B = nil// Both deallocated correctlyweak vs unowned
Section titled “weak vs unowned”weak: Optional, reference can become nil at any time. Use when the referenced object might be deallocated before the reference.unowned: Non-optional, reference is assumed to never become nil during its lifetime. Use when the referenced object outlives the reference. Accessing a deallocatedunownedreference crashes.
// weak -- use with optionalclass Customer { weak var card: CreditCard?}
// unowned -- use when lifecycle is guaranteedclass CreditCard { unowned let owner: Customer init(owner: Customer) { self.owner = owner }}Access Control
Section titled “Access Control”| Level | Same module | Different module |
|---|---|---|
open | Yes (subclass, override) | Yes (subclass, override) |
public | Yes | Yes (use only) |
internal | Yes | No |
fileprivate | Same file | No |
private | Same scope | No |
open class PublicClass { public var name: String internal var id: String fileprivate var secret: String private var password: String
public init(name: String, id: String, secret: String, password: String) { self.name = name self.id = id self.secret = secret self.password = password }}Summary
Section titled “Summary”Swift’s type system separates value types (structs) from reference types (classes), each with distinct semantics. Protocols and extensions enable flexible abstractions without inheritance. Generics provide type-safe reusable code, and ARC manages reference type memory automatically. Understanding these concepts is essential for designing robust, efficient Swift applications.
Intuition
Section titled “Intuition”Swift’s choice between structs and classes is a fundamental design decision. Structs are value types: they copy on assignment and live on the stack for small sizes. Classes are reference types: they share references and live on the heap with ARC managing their lifetime. Use structs by default and switch to classes only when you need inheritance, shared mutable state, or Objective-C interop. Protocol-oriented design often eliminates the need for class inheritance entirely.
Cross-References
Section titled “Cross-References”- [[swift/01-basics/1_variables-and-types]] - Value types versus reference types
- [[swift/02-functions-closures/1_functions]] - Methods, initializers, and closures
- [[swift/04-advanced/1_error-handling]] - Error propagation in class hierarchies
- [[swift/04-advanced/2_concurrency]] - Actors for thread-safe reference types
Common Mistakes
Section titled “Common Mistakes”Using classes when structs would suffice: Classes introduce reference semantics, ARC overhead, and potential retain cycles. Default to structs unless you specifically need inheritance or shared mutable state.
Confusing weak and unowned: weak becomes nil when the referenced object is deallocated; unowned crashes. Use weak when the referenced object may outlive the reference, and unowned only when lifetimes are guaranteed to be equal.
Forgetting mutating on struct methods: Struct methods that modify properties must be marked mutating. Without it, the compiler prevents any property changes. This is a common error when transitioning from classes to structs.