Structs vs Classes in Swift
Picture two ways to carry groceries home: a sturdy basket, or a reusable bag you and a friend both hold. Hand over the basket and your friend walks away with their own apples. Hand over the bag and you are both reaching into the same one.
Swift asks you to make that choice every time you define a type. A struct behaves like the basket and a class behaves like the bag, and the difference reaches further than it first appears: into inheritance, memory, mutability, and identity. This guide walks each of those, then hands you a way to decide for your own types rather than a winner.
What are structs and classes?
A struct is a value type. It groups stored properties and methods under one name, and every assignment or function call hands over an independent copy of its stored values. It gets a memberwise initialiser for free unless you write your own initialiser in the same declaration, and it cannot inherit from another struct.
A class is a reference type. It groups the same things under one name, but every assignment or function call hands over a reference to one shared instance. It can inherit from another class, it never gets a synthesised memberwise initialiser, and it can define a deinit.
Both can conform to protocols, hold stored and computed properties, define methods, and be generic. The shared surface is wide enough that the type keyword often looks like a formality, which is exactly why the differences catch people out.
Quick Comparison
The whole comparison at a glance, before the sections that show each row in code:
| Behaviour | Struct | Class |
|---|---|---|
| Assignment hands over | An independent copy | A shared reference |
| Inheritance | Not available | Single superclass |
| Memory management | Scope-based; ARC only for stored references | ARC reference counting |
| Changing a property | Needs a var instance | Works on a let instance |
| Mutating methods | Marked mutating | No keyword needed |
| Identity comparison | Not applicable | Triple equals operator |
| Memberwise initialiser | Synthesised unless you write your own | Never synthesised |
| Deinitialiser | Not on an ordinary struct | Available |
Every row above describes an ordinary struct, which is what the struct keyword gives you unless you opt out of copying. That opt-out is a specialist tool and it changes two of these rows, including the deinitialiser.
How does assignment behave?
This is the row every other row descends from. The same two lines produce opposite results depending on which keyword you used:
struct Basket {
var apples: Int
}
class Bag {
var apples: Int
init(apples: Int) { self.apples = apples }
}
var myBasket = Basket(apples: 10)
var yourBasket = myBasket
yourBasket.apples = 5
let myBag = Bag(apples: 10)
let yourBag = myBag
yourBag.apples = 5
print(myBasket.apples, yourBasket.apples)
print(myBag.apples, yourBag.apples) // Program output:
// 10 5
// 5 5 The basket split into two independent copies at the assignment. The bag did not: both names describe one object, so a change made through either name is visible through the other.
Passing a value into a function follows the same rule. A struct arrives as a copy, so changes stay inside unless the parameter is marked inout or the struct stores a reference. A class instance arrives as a reference the function can mutate on your behalf.
Which one supports inheritance?
Only classes. A class can name one superclass, inherit its properties and methods, and override them:
class Vehicle {
var speed = 0.0
func accelerate() { speed += 10 }
}
class Car: Vehicle {
override func accelerate() { speed += 20 }
}
let car = Car()
car.accelerate()
print(car.speed) // Program output:
// 20.0 Structs have no equivalent, and that is a smaller loss than it sounds. Protocols plus protocol extensions supply shared behaviour to any number of unrelated types, without the single-parent restriction or the fragile base class problem:
import Foundation
protocol Priced {
var price: Double { get }
}
extension Priced {
var formattedPrice: String { String(format: "$%.2f", price) }
}
struct Book: Priced { let price: Double }
print(Book(price: 12.5).formattedPrice) // Program output:
// $12.50 Reach for a class when you genuinely need a hierarchy, or when an Apple framework hands you a base class to subclass. Reach for a protocol when what you actually wanted was shared behaviour.
How is memory managed?
A class instance lives on the heap and is tracked by Automatic Reference Counting. ARC counts how many references point at the instance and frees it when that count reaches zero:
class Session {
let name: String
init(name: String) {
self.name = name
print("\(name) opened")
}
deinit { print("\(name) closed") }
}
var first: Session? = Session(name: "editor")
var second = first
first = nil
second = nil // Program output:
// editor opened
// editor closed Note where the closing line appears. Setting first to nil did nothing visible, because second was still holding the instance. Only the second assignment dropped the count to zero.
Structs need none of this bookkeeping, because each copy is owned outright by whatever holds it and goes away with its scope. The value itself is not reference-counted, so it cannot form a retain cycle alone, but any reference it stores can still join one that weak or unowned has to break.
What does it take to change a stored value?
Structs are not immutable, and the shorthand that says they are causes real confusion. What is true is narrower: mutability follows the variable that holds the struct, and a method that changes stored properties has to declare it.
struct Rectangle {
var width: Double
var height: Double
mutating func scale(by factor: Double) {
width *= factor
height *= factor
}
}
var box = Rectangle(width: 3, height: 4)
box.scale(by: 2)
print(box.width, box.height)
let fixed = Rectangle(width: 3, height: 4)
// fixed.width = 10 // error: fixed is a let constant // Program output:
// 6.0 8.0 Classes behave differently because let constrains the reference rather than the object. A class instance held in a constant still accepts writes to any var property, and no mutating keyword exists on class methods.
That is the practical asymmetry. With a struct, let means the value cannot change. With a class, let means the name cannot be repointed.
Equality Against Identity
Reference types introduce a question value types do not have: are these the same object, or merely two objects that look alike? Swift gives those two questions different operators.
class Ticket: Equatable {
let code: String
init(code: String) { self.code = code }
static func == (lhs: Ticket, rhs: Ticket) -> Bool { lhs.code == rhs.code }
}
let a = Ticket(code: "A1")
let b = Ticket(code: "A1")
let c = a
print(a == b, a === b)
print(a == c, a === c) // Program output:
// true false
// true true The == operator compares contents using whatever you wrote, and === compares instance identity. For a struct only the first question exists, and Swift can synthesise Equatable for you when every stored property is already Equatable.
What about performance?
The usual claim is that structs are faster. That holds for the common case and needs its scope stated, because the mechanism is not speed but allocation.
A small struct can live in registers or on the stack, with no allocation of its own and no reference counting unless it stores references. A class instance usually costs a heap allocation, and passing references around usually costs retain and release traffic that the optimiser can sometimes remove.
The direction reverses when the struct gets large. Copying a value type with many stored properties through a deep call chain can move real bytes, where a class would have moved one pointer. The compiler may pass a large value indirectly instead, so this is a tendency rather than a guarantee.
Swift softens this for the standard library's collections with copy-on-write, so an Array or String property does not copy its storage until something writes to it.
None of this is a reason to pick a type. Pick on semantics, then measure with Instruments if a hot path turns out to matter.
The Same Model Written Both Ways
A concrete job makes the choice obvious. Here is one screen's data written the way each type is meant to be used:
import Foundation
struct Money: Equatable {
let amount: Decimal
let currency: String
}
final class AccountStore {
private(set) var balance: Money
init(balance: Money) { self.balance = balance }
func apply(_ delta: Decimal) {
balance = Money(amount: balance.amount + delta, currency: balance.currency)
}
}
let store = AccountStore(balance: Money(amount: 100, currency: "AUD"))
let watcher = store
store.apply(-25)
print(watcher.balance == Money(amount: 75, currency: "AUD")) // Program output:
// true Money is a struct because two amounts of seventy-five dollars are the same thing; there is no reason to ask which one is the original. AccountStore is a class because the app has exactly one balance, and every screen that reads it must see the same value.
That is the test in one sentence. Data gets a struct; the one thing everyone shares gets a class.
What Choosing Wrong Costs
Picking a class for plain data produces bugs that look like haunting. A model edited on a detail screen changes the list behind it, because both were holding the same object, and the fix is a defensive copy at every boundary you remember to write one.
Picking a struct for shared state produces the opposite complaint: an update that appears to do nothing. The mutation landed on a copy nobody else is looking at, which shows up in SwiftUI as a value that changes in the debugger and never on screen.
Reversing the decision later is not free. Switching a class to a struct means finding every place that relied on shared mutation, and switching a struct to a class means auditing every copy that silently became an alias. Both are mechanical, tedious, and easy to get half right.
How to Choose Between Them
Choose a struct when:
- The type describes data rather than a participant, such as a point, a price, or a parsed response.
- Two instances with the same contents should count as the same thing.
- Copies being independent is what you want, not something to defend against.
- Shared behaviour can come from protocols instead of a base class.
Choose a class when:
- The type has identity: one network client, one cache, one store, and everyone must see the same one.
- Mutations made in one place must be visible everywhere the instance is held.
- You need inheritance, or an Apple framework hands you a base class to subclass.
- You need a
deinitto release something the type owns, which an ordinary struct cannot give you.
When both lists look plausible, Apple's own recommendation is to start with a struct and move to a class only when a listed class reason actually appears. Starting from the shared-mutable side is harder to walk back.
Frequently Asked Questions
Are structs always faster than classes?
No. A small struct avoids allocation and reference counting, which usually wins. A struct with many stored properties copied through deep call chains can cost more than passing one reference, so measure before treating it as a rule.
Can a struct conform to a protocol?
Yes, and this is how structs get shared behaviour without inheritance. A protocol extension supplies a default implementation to every conforming type, which covers most of what a base class would have been used for.
Why can I change a property on a let class instance?
The let applies to the reference, not to what it points at. The constant guarantees the name keeps pointing at the same instance; any var property inside that instance is still free to change.
Can a struct hold a class as a property?
Yes, and it is worth knowing what that does. Copying the struct copies the reference, so both copies share the same object. The struct then has value semantics on paper and shared mutable state in practice.
What about actors and enums?
An enum is a value type and copies like a struct. An actor is a reference type like a class, with the extra rule that code outside its isolation reaches its mutable state through await. Code already running on the actor reads it synchronously, and that isolation is how it protects itself from data races.
Does SwiftUI prefer one over the other?
SwiftUI views are structs, and the framework recreates them freely. Long-lived state that several views share is usually a class marked @Observable, because SwiftUI needs a stable instance to watch rather than a fresh copy.
Self-Check
- After
var b = a, when does changingbalso changea? - Why does a struct method that writes to a stored property need the
mutatingkeyword? - What does
===compare that==does not? - Which of the two can form a retain cycle, and what breaks it?
- A struct holds a class instance as a property. What happens when the struct is copied?
- Name one reason to pick a class that has nothing to do with performance.
Answers
- When the type is a class. A class assignment copies the reference, so both names describe one instance. A struct assignment copies the value.
- Because the method may be called on a copy the caller owns. The keyword marks the method as requiring a
varinstance, so calling it on aletis a compile error rather than a silent no-op. - Instance identity. Two distinct objects can be
==while===isfalse; only===answers whether they are the same allocation. - Classes. Two instances holding strong references to each other never reach a zero count. Marking one side
weakorunownedbreaks the cycle. - The reference is copied, not the object. Both struct copies point at the same instance, so mutating through one is visible through the other.
- Identity. When the app must have exactly one of something, or when a framework requires a subclass, or when a
deinitis needed to release an owned resource.
Where to Go Next
The basket and the bag come down to one question: does handing something over create a second copy, or share the first? Every other difference in this guide follows from that answer.
Ask what the type is before asking what it costs. If it is a fact about the world, copy it; if it is a thing the app owns exactly one of, share it.
Related
- Understanding typealias in Swift - name a type once, whichever kind you chose.
- Sets in Swift - what value semantics mean for elements stored in a collection.
- Swift - the language hub and its learning path.
Sources
-
[1]
Structures and Classes(docs.swift.org)
-
[2]
Automatic Reference Counting(docs.swift.org)
-
[3]
Choosing Between Structures and Classes(developer.apple.com)
Read Next
The typealias keyword gives an existing type a second name. What it does to closures, tuples, dictionaries, and generics, what it deliberately does not do, and the errors it produces.
A Swift Set stores unique values with no order and answers membership questions in constant time on average. How to create one, run set algebra, make your own types storable, and decide between Set and Array.
A practical Swift guide to optionals, value types, closures, async/await, SwiftUI state, and reviewing AI-assisted Apple platform code.