Classes & Reference Types
Reviewed & published by Brayan K
Classes bring reference semantics, inheritance, and lifecycle hooks. By the end of this lesson you'll define classes, write init/deinit, use inheritance with override, and clearly see how a class differs from a struct.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • Define classes with class and write your own init
- • Understand reference semantics: variables share one instance
- • Use the lifecycle hooks init and deinit
- • Build hierarchies with inheritance and override
- • Decide between struct and class with confidence
1️⃣ Defining a Class
Declare a class with its properties and methods. Unlike structs, classes have no free memberwise initializer — you write your own init to set up stored properties.
// A CLASS bundles data and behaviour — but unlike a struct it has no
// free memberwise initializer, so you write your own init.
class Dog {
var name: String
var age: Int
init(name: String, age: Int) { // designated initializer
self.name = name
self.age = age
}
func bark() {
print("\(name) says woof!")
}
}
let rex = Dog(name: "Rex", age: 3)
rex.bark() // Rex says woof!
print("\(rex.name) is \(rex.age)") // Rex is 32️⃣ Reference Semantics (Shared Instances)
This is the heart of classes: assigning one to a new variable makes both refer to the same object. A change through one is seen through the other — the opposite of a struct's independent copy.
// REFERENCE SEMANTICS: variables SHARE the same instance.
class Counter {
var count = 0
}
let a = Counter()
let b = a // b refers to the SAME object as a
b.count = 5 // change through b ...
print(a.count) // 5 — a sees it too! (shared instance)
print(b.count) // 5
// Note: 'a' is a let, yet we can still change a.count, because let fixes
// the REFERENCE, not the object's mutable properties.
// === Contrast with a struct (value type) ===
struct SCounter { var count = 0 }
var x = SCounter()
var y = x // y is a COPY
y.count = 5
print(x.count) // 0 — copies are independent
print(y.count) // 53️⃣ Lifecycle: init & deinit
init runs when you create an instance; deinit runs automatically just before the instance is freed — perfect for cleanup like closing files or removing observers.
// init runs when an instance is created; deinit runs just before it's freed.
class FileHandle {
let name: String
init(name: String) {
self.name = name
print("Opened \(name)")
}
deinit {
print("Closed \(name)") // cleanup before deallocation
}
}
func useFile() {
let f = FileHandle(name: "data.txt")
print("Using \(f.name)")
// when useFile() ends, f goes out of scope and deinit runs
}
useFile()4️⃣ Inheritance & override
Only classes can inherit. A subclass extends a superclass and replaces inherited methods with override, enabling polymorphism — treating different subclasses through a shared base type.
// Classes support INHERITANCE. A subclass extends a superclass.
class Animal {
var name: String
init(name: String) { self.name = name }
func speak() -> String { return "..." }
}
class Cat: Animal { // Cat inherits from Animal
override func speak() -> String { // 'override' replaces the parent method
return "Meow"
}
}
class Cow: Animal {
override func speak() -> String { return "Moo" }
}
let animals: [Animal] = [Cat(name: "Tom"), Cow(name: "Bess")]
for a in animals {
print("\(a.name): \(a.speak())") // polymorphism in action
}Your turn. Fill in the blanks, then run it and check the output.
// 🎯 YOUR TURN — replace each ___ then run it in SwiftFiddle.
// 1) Define a class 'Person' with a stored property 'name' (String).
___ Person { // 👉 replace ___ with the keyword class
var name: String
// 2) Write the initializer that sets self.name.
init(name: String) {
___ = name // 👉 replace ___ with self.name
}
func greet() {
print("Hi, I'm \(name)")
}
}
let p = Person(name: "Ada")
p.greet()
// 3) Make a second variable that REFERENCES the same instance, then rename it.
let q = p
q.name = "Grace"
p.greet() // shows the shared change
// ✅ Expected output:
// Hi, I'm Ada
// Hi, I'm GracePro Tips
- 💡 Default to structs. Use a class only when you need shared mutable state, identity, or inheritance.
- 💡 Watch shared mutation. Because references share one object, changes ripple to every holder — powerful, but a common source of bugs.
- 💡 Call super.init from a subclass initializer to set up the inherited part.
- 💡 Use === to test whether two references point at the very same instance (identity).
Common Errors (and the fix)
- "Class 'Dog' has no initializers" — classes don't auto-generate one. Write an init that sets every stored property.
- "Property 'self.x' not initialized at super.init call" — set your own properties before calling super.init.
- "Overriding declaration requires an 'override' keyword" — add override when replacing an inherited method.
- Unexpected shared changes — two variables pointing at one instance both see edits. If you wanted independence, use a struct.
📋 Quick Reference
| Feature | struct | class |
|---|---|---|
| Semantics | value (copy) | reference (shared) |
| Memberwise init | yes (free) | no (write your own) |
| Inheritance | no | yes |
| deinit | no | yes |
| Mutating keyword | required | not needed |
| Identity (===) | n/a | yes |
Mini-Challenge: Vehicles with Inheritance
Build it, run it, and check your output against the example in the comments.
// 🎯 MINI-CHALLENGE: Vehicles with inheritance
// 1. Define a class 'Vehicle' with a property 'wheels' (Int) and an init,
// plus a method describe() -> String returning "A vehicle with N wheels".
// 2. Define 'Car: Vehicle' that overrides describe() to return
// "A car with 4 wheels" (set wheels to 4 in its init via super.init).
// 3. Define 'Motorbike: Vehicle' similarly with 2 wheels.
// 4. Put a Car and a Motorbike in an [Vehicle] array and print each describe().
//
// ✅ Example output:
// A car with 4 wheels
// A motorbike with 2 wheels
// your code here🎉 Lesson Complete!
- ✅ class defines a reference type — variables share one instance
- ✅ Write your own init; use deinit for cleanup
- ✅ Classes support inheritance with override
- ✅ struct = value/copy; class = reference/shared — choose deliberately
Practice quiz
What keyword defines a class?
- struct
- object
- class
- type
Answer: class. Classes are declared with the class keyword.
Are classes value types or reference types?
- Reference types
- Value types
- Neither
- Both
Answer: Reference types. Classes are reference types — variables share the same instance.
Given let a = Box(); let b = a; b.value = 9 — what is a.value?
- unchanged old value
- nil
- compile error
- 9 (a and b share the same instance)
Answer: 9 (a and b share the same instance). Both a and b reference the same object, so a.value is also 9.
Do classes get a free memberwise initializer like structs?
- Yes
- No — you must write your own init
- Only with @auto
- Only for one property
Answer: No — you must write your own init. Classes require you to write an initializer for stored properties without defaults.
What is a deinitializer (deinit) used for?
- Cleanup right before an instance is freed
- Creating instances
- Copying instances
- Comparing instances
Answer: Cleanup right before an instance is freed. deinit runs just before a class instance is deallocated, for cleanup.
Which feature do classes support that structs do not?
- Methods
- Computed properties
- Inheritance from another class
- Protocols
Answer: Inheritance from another class. Only classes support inheritance from a superclass.
What keyword marks a method as overriding a superclass method?
- super
- override
- virtual
- replace
Answer: override. Subclasses use the override keyword to replace inherited methods.
Do you need 'mutating' to change a class property inside a method?
- Yes, always
- Only for let properties
- Only in subclasses
- No — classes are reference types, so it's not needed
Answer: No — classes are reference types, so it's not needed. mutating is a value-type concept; class methods can change properties freely.
Can you change a property of a class instance stored in a `let` constant?
- No, never
- Yes — let fixes the reference, not the object's var properties
- Only the first property
- Only with override
Answer: Yes — let fixes the reference, not the object's var properties. A let class reference is fixed, but the instance's var properties can still change.
When should you choose a class over a struct?
- For all data
- Never
- When you need shared mutable state, identity, or inheritance
- Only for numbers
Answer: When you need shared mutable state, identity, or inheritance. Classes suit reference semantics: shared state, identity, and inheritance.
Continue this course
- Previous: Structs & Value Types
- Next: Properties: Stored, Computed & Observers — Stored vs computed properties, willSet/didSet, lazy and static
Frequently asked questions
What is the difference between a class and a struct?
The core difference is semantics: classes are reference types and structs are value types. Assigning a class instance to another variable makes both point at the SAME object, so a change through one is visible through the other. Assigning a struct makes an independent copy. Classes also support inheritance, deinitializers, and identity checks (===); structs do not.
Why don't classes get a free memberwise initializer?
Because classes participate in inheritance, Swift can't safely synthesize a memberwise initializer the way it does for structs. You must write your own designated initializer that assigns every stored property without a default value. If a subclass adds properties, it provides its own init and calls super.init to set up the inherited part.
What is a deinitializer and when does it run?
A deinitializer (deinit) is special method on a class that runs automatically just before an instance is deallocated — when nothing references it any more. It's the place to release resources like file handles, observers, or timers. Structs don't have deinitializers because value types aren't reference-counted in the same way.
Can I change a property of a class instance held in a 'let'?
Yes — and this surprises people coming from structs. A let on a class fixes the reference (you can't point it at a different object), but the object's own var properties can still be changed. With a struct, a let makes the whole value immutable. This is a direct consequence of reference vs value semantics.
When should I use a class instead of a struct?
Choose a class when you need reference semantics: shared mutable state that several parts of your app see, object identity (is this the very same instance?), inheritance, or interoperability with Objective-C frameworks. For most data models, Apple recommends a struct; reach for a class deliberately when one of those reference-type features is genuinely required.