LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 818 min read

Structs & Classes

Understand value vs reference semantics, mutating methods, initializers, inheritance, deinitializers, and when to choose a struct over a class.

Introduction

Structs and classes are Swift's two main tools for grouping related data and behavior into a custom type. They look similar at first glance — both can have properties, methods, and initializers — but they differ in one fundamental way: how copies of them behave. Getting this distinction right is one of the most important skills in writing correct Swift.

This lesson covers defining both, the value-vs-reference semantics that separate them, mutating methods, inheritance (classes only), deinitializers, and practical guidance on which to reach for.

Defining Structs

A `struct` bundles related properties together. Swift automatically generates a memberwise initializer for structs, so you don't have to write one yourself just to set initial values.

struct Point {
var x: Double
var y: Double
}
var p1 = Point(x: 0, y: 0)
var p2 = p1
p2.x = 10
print(p1.x)
print(p2.x)
Output

Click Run to see what this code prints.

Assigning `p1` to `p2` created an entirely independent copy — changing `p2.x` had no effect on `p1.x` at all. This is value semantics, and it's the default behavior for every struct in Swift.

Defining Classes

A `class` looks almost identical to a struct at first glance, but classes do not get a free memberwise initializer, and — critically — instances are shared by reference rather than copied.

class Counter {
var count = 0
func increment() {
count += 1
}
}
let c1 = Counter()
let c2 = c1
c2.increment()
print(c1.count)
print(c2.count)
Output

Click Run to see what this code prints.

Value vs Reference Semantics

This is the single most important distinction between the two: assigning or passing a struct copies its entire value, so each copy is fully independent. Assigning or passing a class instance copies only the reference — both variables point at the exact same underlying object in memory, so a change through one is visible through the other, as the `Counter` example above demonstrated.

Mutating Methods

Because struct instances are values, a method on a struct cannot modify its own properties unless it's explicitly marked `mutating`. This makes accidental mutation impossible by default — the compiler simply won't allow it without the keyword.

struct Point2 {
var x: Double
var y: Double
mutating func moveBy(dx: Double, dy: Double) {
x += dx
y += dy
}
}
var origin = Point2(x: 0, y: 0)
origin.moveBy(dx: 5, dy: 5)
print(origin.x, origin.y)
Output

Click Run to see what this code prints.

Initializers

Structs get a free memberwise initializer automatically, generated from their stored properties. Classes never get one automatically — you must write an `init` yourself for any properties that don't have a default value.

class Animal {
var name: String
init(name: String) {
self.name = name
}
}

Inheritance

Only classes support inheritance in Swift — structs cannot inherit from another struct. A subclass can override a superclass's methods using the `override` keyword, which the compiler requires so overrides are always intentional and visible.

class Dog: Animal {
override func speak() -> String {
"\(name) says Woof!"
}
}
class Animal2 {
var name: String
init(name: String) { self.name = name }
func speak() -> String { "\(name) makes a sound." }
}
class Dog2: Animal2 {
override func speak() -> String { "\(name) says Woof!" }
}
let pet: Animal2 = Dog2(name: "Rex")
print(pet.speak())
Output

Click Run to see what this code prints.

Deinitializers

Classes can define a `deinit` block, which Swift automatically calls right before an instance is deallocated by Automatic Reference Counting (ARC) — useful for cleanup like closing a file handle or invalidating a timer. Structs never have a `deinit` because, as value types, there's no shared instance lifetime to track — each copy simply exists on its own.

Identity Operators

Because class instances are references, Swift provides `===` and `!==` to check whether two variables point to the exact same instance in memory — this is different from `==`, which (when implemented via `Equatable`) compares whether two instances have equal values.

let counterA = Counter()
let counterB = counterA
let counterC = Counter()
print(counterA === counterB) // true — same instance
print(counterA === counterC) // false — different instances

Choosing Struct or Class

Use a struct when...Use a class when...
You want independent copies with no shared mutable state.You need shared, mutable state across multiple owners.
The data models a simple value (a point, a color, a size).You need identity — two instances that are "the same object."
You don't need inheritance.You need to share behavior through class inheritance.
Most Swift standard library types (Array, String, Dictionary).You need a deinitializer to clean up resources.

Common Mistakes

Avoid These Mistakes
  • Assuming a struct assignment shares state like a class would — it always copies the full value.
  • Forgetting the `mutating` keyword on a struct method that changes `self`, which is a compile-time error.
  • Forgetting `override` when a subclass replaces a superclass method — Swift requires it explicitly.
  • Defaulting to classes out of habit from other object-oriented languages, when a struct would be simpler and safer.

Best Practices

  • Prefer structs by default, as Apple recommends and as most of the Swift standard library itself does.
  • Reach for a class specifically when you need reference semantics, identity, or inheritance — not as a default habit.
  • Mark every struct method that modifies its own properties with `mutating`.
  • Use `===`/`!==` only for identity checks on classes; use `==` (via Equatable) to compare values.

Frequently Asked Questions

Yes, freely. A struct can hold a class instance as a property (that property is still a shared reference), and a class can hold struct properties (which are copied whenever the class instance itself is passed by reference, but not independently).

Value semantics make code easier to reason about — you never have to worry about a struct changing unexpectedly because some other part of the code holds a reference to it, which eliminates a whole category of shared-mutable-state bugs.

No, but they can conform to multiple protocols, which covers most of the code-reuse benefits people reach for inheritance for — covered in the Protocols & Extensions lesson.

Key Takeaways

  • Structs are value types — assigning or passing one always creates an independent copy.
  • Classes are reference types — assigning or passing one shares the same underlying instance.
  • Struct methods that modify properties must be marked `mutating`.
  • Only classes support inheritance, `override`, and `deinit`.
  • Prefer structs by default; use classes when you specifically need identity, shared mutable state, or inheritance.

Summary

You now understand the crucial value-versus-reference distinction between structs and classes, and when to reach for each. Next, you'll learn how enums let you model a fixed set of related states with remarkable type safety.

Next Lesson →

Enums