OOP in Swift
Struct vs class value/reference semantics, protocols, extensions, and protocol-oriented programming.
Structs vs. classes — value vs. reference semantics
This is the single most important distinction in Swift's type system, and it has no exact equivalent in Java or Kotlin. Swift gives you two ways to define a custom type — struct and class — and they differ fundamentally in how they're copied and shared:
- A struct has value semantics — when you assign it to a new variable or pass it to a function, you get an independent copy. Changing the copy never affects the original.
- A class has reference semantics — when you assign it to a new variable or pass it to a function, you get another reference to the same underlying instance. Changing it through either reference is visible through both.
struct PointStruct {
var x: Int
var y: Int
}
var p1 = PointStruct(x: 1, y: 2)
var p2 = p1 // p2 is an independent COPY of p1
p2.x = 99
print(p1.x) // 1 — unaffected by the change to p2
print(p2.x) // 99
class PointClass {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
let c1 = PointClass(x: 1, y: 2)
let c2 = c1 // c2 refers to the SAME instance as c1
c2.x = 99
print(c1.x) // 99 — c1 sees the change too, because c1 and c2 are the same object
print(c2.x) // 99
Notice c1 is a let constant, yet c1.x was still mutated — let on a class reference only prevents reassigning what instance c1 points to, not mutating that instance's properties. This is a common early source of confusion.
When to use which
| Struct | Class | |
|---|---|---|
| Semantics | Value (copied) | Reference (shared) |
| Inheritance | No | Yes |
Identity (===) |
Not applicable | Yes — can check if two variables refer to the same instance |
| Typical use | Data models, coordinates, configuration — most everyday types | Shared, mutable state; anything needing identity or inheritance |
Apple's own guidance — and Swift's standard library itself (Array, String, Dictionary are all structs) — favors structs by default, reaching for a class only when you specifically need reference semantics, shared mutable state, or inheritance.
Protocols
A protocol defines a set of methods and properties a conforming type must implement — Swift's equivalent of an interface. Both structs and classes can conform to protocols:
protocol Describable {
func describe() -> String
}
struct Car: Describable {
var model: String
func describe() -> String {
"This is a \(model)"
}
}
struct Book: Describable {
var title: String
func describe() -> String {
"This is the book '\(title)'"
}
}
func printDescription(_ item: Describable) {
print(item.describe())
}
printDescription(Car(model: "Model 3")) // This is a Model 3
printDescription(Book(title: "1984")) // This is the book '1984'
Extensions
An extension adds new functionality to an existing type — even one you don't own the source code for, like Int or String from the standard library:
extension Int {
var isEven: Bool {
self % 2 == 0
}
func squared() -> Int {
self * self
}
}
print(5.isEven) // false
print(4.squared()) // 16
Extensions are also how you commonly add protocol conformance to an existing type without touching its original definition:
extension Car: Equatable {
static func == (lhs: Car, rhs: Car) -> Bool {
lhs.model == rhs.model
}
}
Protocol-oriented programming
Swift's standard library and idiomatic app code lean toward protocol-oriented programming: rather than building deep class inheritance trees, you define small, focused protocols and share default behavior through protocol extensions:
protocol Greetable {
var name: String { get }
}
extension Greetable {
// a default implementation — any conforming type gets this for free
func greet() -> String {
"Hello, \(name)!"
}
}
struct Person: Greetable {
var name: String
}
struct Robot: Greetable {
var name: String
// overrides the protocol extension's default
func greet() -> String {
"BEEP BOOP, \(name.uppercased()) ONLINE"
}
}
print(Person(name: "Ali").greet()) // Hello, Ali!
print(Robot(name: "R2D2").greet()) // BEEP BOOP, R2D2 ONLINE
Because both structs and enums (not just classes) can conform to protocols, and protocol extensions can supply default, shareable implementations, protocol-oriented programming gives you composition-style code reuse across value types — something class inheritance alone can't do, since only classes support inheritance.
Common mistakes
- Reaching for a
classby default (habit from Java/C#) when astructwould give safer, more predictable value semantics — Apple's own guidance is to default to structs. - Being surprised that mutating a
classinstance through oneletreference is visible through every other reference to it — that's reference semantics working as intended, not a bug. - Building a deep class inheritance hierarchy where a small protocol plus a protocol extension with default behavior would be a flatter, more flexible fit.
Interview questions
Q: What is the core difference between a struct and a class in Swift? Structs have value semantics — assigning or passing one creates an independent copy, so mutating the copy never affects the original. Classes have reference semantics — assigning or passing one shares the same underlying instance, so a mutation through any reference is visible through all of them.
Q: Why does Apple recommend defaulting to structs over classes?
Value semantics make code easier to reason about — you don't need to worry about a struct being unexpectedly mutated somewhere else in the code through a shared reference. Swift's own standard library collections (Array, Dictionary, String) are all structs for exactly this reason; classes are reserved for cases that specifically need shared mutable state or inheritance.
Q: What is protocol-oriented programming, and what problem does it solve? It's a style that favors composing small, focused protocols — with shared default behavior supplied via protocol extensions — over deep class inheritance hierarchies. Because structs and enums can conform to protocols (but can't inherit from a class), this lets value types share reusable behavior the way only classes could through inheritance in a purely object-oriented approach.