Skip to content
SwiftMind

Struct vs Class

Value semantics versus reference semantics, and why Swift pushes you toward structs.

BeginnerNot started8 min
On this page

A struct is a *value*. When you assign it or pass it to a function, you get an independent copy. Changing the copy can't affect the original.

A class is a *reference*. When you assign it, both names point at the same object in memory. Changing it through one name changes it for everyone holding a reference.

Everything else — inheritance, deinit, reference counting, identity — follows from that one difference. Swift makes structs the default because most of the time, shared mutable state is a liability you didn't ask for.

The let difference surprises people more than the copy does.
struct PointValue { var x = 0 }final class PointRef { var x = 0 }
var a = PointValue(); var b = ab.x = 10// a.x == 0   — b is an independent copy
let c = PointRef(); let d = cd.x = 10// c.x == 10  — same object, and note `let` didn't stop the mutation
// `let` on a struct freezes its contents; on a class it only freezes the pointerlet frozen = PointValue()// frozen.x = 1        // error: frozen is a let constantlet stillMutable = PointRef()stillMutable.x = 1     // fine — the reference is constant, the object isn't
Show all 16 lines
Copy versus share
var a
var b
x = 0a's own storage
x = 10b's own storage
let c
let d
x = 10one shared object

Two struct variables a and b each point at their own separate storage. Two class variables c and d both point at one shared object.

Assigning a struct copies the storage — two variables, two boxes.

Step 1 of 2
  1. Assigning a struct copies the storage — two variables, two boxes.
  2. Assigning a class copies the pointer — two variables, one box.

Under the hood

Structs don't automatically live on the stack, and classes aren't automatically slow. The real distinctions are:

Allocation. A class instance is always a heap allocation with a reference count and a type header. A struct is stored inline wherever it lives — on the stack as a local, or inside its owner's memory as a property. A struct property inside a class instance is on the heap too.

Copy on write. Standard-library collections are structs, but copying an Array doesn't copy its elements. Both copies share one buffer, and the buffer is only duplicated when one of them is mutated *and* the reference count says it isn't uniquely held. That's why value semantics stay affordable at scale — and you can implement the same trick yourself with isKnownUniquelyReferenced.

Dispatch. A method on a final class or a struct can be called directly. A non-final class method goes through a vtable so subclasses can override it.

Quick check

Why does stillMutable.x = 1 compile when stillMutable is declared with let?

  • `let` only applies to structs
  • `let` makes the reference constant, not the object it points to; the object's own vars are still mutable
  • Classes ignore `let` entirely
  • It doesn't compile
Reveal answer

`let` makes the reference constant, not the object it points to; the object's own vars are still mutable

For a class, the constant is the pointer. You can't reassign stillMutable to a different instance, but you can mutate the instance it points at. For a struct, the value *is* the storage, so let freezes the whole thing including its properties. This is the single most common source of accidental shared mutation.

Interview question

How do you decide between a struct and a class for a new type?

Think first, then reveal

I start from struct and require a reason to move.

I move to a class when I need identity — when "is this the same object?" is a meaningful question distinct from "are these equal?". A user session, a network connection, a view controller: two of them with identical fields are still not interchangeable.

Also when I need shared mutable state that several owners must see change, when I need deinit to release something (an observer, a file handle, a socket), when I need inheritance or Objective-C interop, or when the object is genuinely long-lived and referenced from many places.

Everything else — models decoded from JSON, geometry, configuration, anything I want to pass across threads without worrying — is a struct. Value semantics mean I can hand it to another part of the app and know nobody can mutate it behind my back, which removes a whole category of bug rather than requiring me to defend against it.

I don't use size as a tiebreaker, because copy-on-write already handles large payloads.

What an interviewer listens for

  • Defaults to struct and justifies the exception rather than the rule
  • Names identity as the core distinction, not performance
  • Mentions deinit, inheritance and ObjC interop as concrete class-only needs
  • Knows copy-on-write removes the "too big to copy" argument
  • Connects value semantics to thread safety

Key takeaway

Default to a struct. Reach for a class when you specifically need identity, shared mutation, inheritance, or deinit — and know that you're taking on reference counting and the possibility of cycles when you do.

Related

0%
Quiz this concept