Classes vs Structs
Classes vs Structs
Swift provides two primary building blocks for creating custom types: classes (reference types) and structs (value types). Both can have properties, methods, initializers, and conform to protocols.
Defining a Struct
struct Point {
var x: Double
var y: Double
func distance(to other: Point) -> Double {
let dx = x - other.x
let dy = y - other.y
return sqrt(dx * dx + dy * dy)
}
}
let p1 = Point(x: 0, y: 0)
let p2 = Point(x: 3, y: 4)
p1.distance(to: p2) // 5.0
Structs receive a memberwise initializer automatically:
let p3 = Point(x: 1.0, y: 2.0) // auto-generated init
Defining a Class
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func greet() -> String {
return "Hello, I'm \(name)"
}
}
let person = Person(name: "Alice", age: 30)
person.greet() // "Hello, I'm Alice"
Classes require explicit initializers. They do not get memberwise initializers.
Inheritance
Classes support single inheritance:
class Student: Person {
var grade: String
init(name: String, age: Int, grade: String) {
self.grade = grade
super.init(name: name, age: age)
}
override func greet() -> String {
return "Hi, I'm \(name), a \(grade) student"
}
}
let student = Student(name: "Bob", age: 15, grade: "A")
student.greet() // "Hi, I'm Bob, a A student"
Use override to override parent methods. Use super to call the parent implementation.
When to Use Which?
| Feature | Struct | Class |
|---|---|---|
| Type | Value type | Reference type |
| Inheritance | No | Yes (single) |
| Deinitializer | No | Yes |
| Memberwise init | Auto-generated | No |
| Identity | No (===) |
Yes |
| Copy behavior | Deep copy | Reference copy |
Apple's recommendation: Prefer structs unless you specifically need class features (inheritance, reference semantics, deinitializers).
Properties
Both structs and classes support stored and computed properties:
struct Rectangle {
var width: Double
var height: Double
var area: Double { // computed property
return width * height
}
var perimeter: Double {
return 2 * (width + height)
}
}
let r = Rectangle(width: 10, height: 5)
r.area // 50.0
r.perimeter // 30.0
Methods
Both support instance methods and static methods:
struct Calculator {
static func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
}
Calculator.add(2, 3) // 5
Classes additionally support class methods (overridable static methods).
Enums & Pattern Matching
Enums & Pattern Matching
Enums define a group of related values, enabling type-safe code without raw strings or integer constants.
Basic Enums
enum Direction {
case north, south, east, west
}
var heading: Direction = .north
heading = .east // no need to repeat Direction
Enums with Associated Values
Each case can hold different associated data:
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
let product = Barcode.qrCode("ABCDEFG")
switch product {
case .upc(let system, let manufacturer, let product, let check):
print("UPC: \(system)-\(manufacturer)-\(product)-\(check)")
case .qrCode(let code):
print("QR: \(code)")
}
// "QR: ABCDEFG"
Enums with Raw Values
Enums can have underlying raw values:
enum Planet: Int {
case mercury = 1, venus, earth, mars
}
let earth = Planet(rawValue: 3) // Optional(.earth)
let position = Planet.earth.rawValue // 3
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
let method = HTTPMethod.get
print(method.rawValue) // "GET"
Pattern Matching
Swift's switch statement supports powerful pattern matching:
let number = 42
switch number {
case 0:
print("zero")
case 1...9:
print("single digit")
case 10...99:
print("double digit")
default:
print("large number")
}
// "double digit"
Tuple pattern matching:
let point = (2, 0)
switch point {
case (0, 0):
print("origin")
case (_, 0):
print("on x-axis")
case (0, _):
print("on y-axis")
case (-2...2, -2...2):
print("near origin")
default:
print("elsewhere")
}
// "on x-axis"
where clauses:
let temp = 72
switch temp {
case ..<32 where temp > 0:
print("freezing but above zero")
case 32...:
print("above freezing")
default:
print("below freezing")
}
if case (pattern matching without switch):
let result: Result<Int, Error> = .success(42)
if case .success(let value) = result {
print("Got: \(value)")
}
Enums are one of Swift's most powerful features. They combine type safety with pattern matching to create expressive, maintainable code.
Value vs Reference Types
Value vs Reference Types
Understanding the distinction between value types and reference types is fundamental to writing correct Swift code.
Value Types
A value type is copied when assigned or passed to a function. Each copy is independent.
Swift's value types include:
- Structs (including Array, Dictionary, Set)
- Enums
- Tuples
- All basic types (Int, String, Bool, Double)
struct Temperature {
var celsius: Double
var fahrenheit: Double { return celsius * 9/5 + 32 }
}
var t1 = Temperature(celsius: 100)
var t2 = t1 // copy
t2.celsius = 0
print(t1.celsius) // 100 — unchanged
print(t2.celsius) // 0
Reference Types
A reference type is not copied; both variables refer to the same instance in memory.
Swift's reference types:
- Classes
- Closures (capture by reference)
- Functions
class Thermometer {
var celsius: Double
init(celsius: Double) { self.celsius = celsius }
}
var t1 = Thermometer(celsius: 100)
var t2 = t1 // same instance
t2.celsius = 0
print(t1.celsius) // 0 — changed!
print(t2.celsius) // 0
Identity Operators
Use === and !== to check if two references point to the same instance:
let a = Thermometer(celsius: 100)
let b = a
let c = Thermometer(celsius: 100)
a === b // true — same instance
a === c // false — different instances
a !== c // true
Copy-on-Write (COW)
Swift collections (Array, Dictionary, Set, String) use copy-on-write optimization. They appear to be copied but share storage until one is mutated:
var a = [1, 2, 3]
var b = a // shares storage
// Both point to same backing store
b.append(4) // NOW a deep copy occurs
// a = [1, 2, 3] (unchanged)
// b = [1, 2, 3, 4]
This gives you value semantics with the performance of reference types when copies aren't mutated.
When to Use Which?
Use structs when:
- You want independent copies
- The type represents a value (point, color, date range)
- You don't need inheritance
Use classes when:
- You need inheritance
- You need reference semantics (shared mutable state)
- You need deinitializers
- You need identity comparison (
===)
Practical Example
struct Money {
var amount: Double
var currency: String
func display() -> String {
return "\(currency) \(amount)"
}
}
class BankAccount {
var owner: String
var balance: Double
init(owner: String, balance: Double) {
self.owner = owner
self.balance = balance
}
func deposit(_ amount: Double) {
balance += amount
}
}
// Money is a value type — copying creates independence
let price1 = Money(amount: 10, currency: "USD")
var price2 = price1
price2.amount = 20
print(price1.amount) // 10 — unchanged
// BankAccount is a reference type — copying shares the instance
let account1 = BankAccount(owner: "Alice", balance: 1000)
let account2 = account1
account2.deposit(500)
print(account1.balance) // 1500 — shared state changed
Understanding when values are copied versus shared is essential for avoiding subtle bugs, especially in concurrent code and UI state management.
Quiz
1. What is the main difference between a struct and a class?
2. Which Swift types support inheritance?
3. What does `===` check?
4. What is copy-on-write?
5. When should you prefer a struct over a class?
Flashcards
Question
What is a value type?
Click to reveal answer
Answer
A type that is copied when assigned or passed. Each copy is independent. Examples: structs, enums, Int, String.
Question
What is a reference type?
Click to reveal answer
Answer
A type where multiple variables point to the same instance. Changes to one affect all references. Examples: classes, closures.
Question
How do you define an enum with associated values?
Click to reveal answer
Answer
Each case can have associated data: `case upc(Int, Int, Int, Int)`
Question
What does `super.init()` do?
Click to reveal answer
Answer
Calls the designated initializer of the parent class in a subclass initializer.
Question
When does copy-on-write trigger a deep copy?
Click to reveal answer
Answer
Only when a shared-storage copy is actually mutated, not when it's just assigned.
Revision Notes
Key Takeaways
- 1. Structs are value types; classes are reference types
- 2. Only classes support inheritance in Swift
- 3. Enums can have associated values and support pattern matching
- 4. Copy-on-write shares storage until mutation occurs
- 5. Prefer structs unless you need class-specific features
Interview Tips
- • Explain value vs reference semantics with examples
- • Know when to use struct vs class
- • Describe copy-on-write optimization
- • Be able to implement enum pattern matching
Cheat Sheet
OOP in Swift Cheat Sheet
Struct:
- Value type, copied on assignment
- Auto memberwise init
- No inheritance
Class:
- Reference type, shared on assignment
- Manual init required
- Supports inheritance, deinitializers,
===
Enum:
- Value type, defines related cases
- Supports associated values and raw values
- Pattern matching with switch
Key Rules:
- Prefer structs by default
- Use classes for inheritance or shared mutable state
===checks reference identity (classes only)- Copy-on-write optimizes collection copying