Structures are copyable value types by default, so assignment and parameter passing create logically independent values; classes are reference types, so several references can point to one instance.
Copying a structure does not recursively copy class instances stored inside it, and a let class reference does not freeze the instance’s mutable properties.
Start with a structure; choose a class only when the model needs shared identity, inheritance, or an ARC-managed lifetime, and inspect ownership for every nested reference.
What it is and why it exists
Structures and classes both organize data and behavior into a named type. Both can declare properties, methods, subscripts, and initializers, conform to protocols, and gain capabilities through extensions. The design choice comes from what happens when a value is assigned, passed, and mutated, not from this shared syntax.
An ordinary structure has value semantics . Assigning a structure to another variable or passing it to a function produces a logical copy; subsequently mutating one value must not change the other’s observable state. A “logical copy” does not require the runtime to duplicate every byte immediately, because the compiler and standard library may share unobservable internal storage.
A class has reference semantics.
A variable holds a reference to an instance, and copying that reference does not create an instance; several callers can read and write the same object through different names.
Class instances also have reference identity , so === and !== tell whether two references point to the same instance.
This distinction supports two different modeling needs. Money, coordinates, configuration snapshots, and parse results usually represent “what this value is” and suit independent copying; sessions, connections, controllers, and resources with explicit lifetimes often represent “which object this is” and may need shared identity. If a rule needs only equality and not identity, convenience of mutation alone is not a reason to introduce shared references.
You encounter this choice in API models, collection elements, SwiftUI state, caches, protocol implementations, and concurrency boundaries. It is not a size rule in which structures are small and classes are large, nor a simplified stack-versus-heap rule. Choose semantics and ownership first, then address performance with measurements.
How it works
When you declare a structure value, the variable represents that value directly. Assignment of a copyable structure establishes a new logical value, and function parameters receive values by the same principle. An implementation may eliminate copies, store fields inline, or use a shared buffer for a collection as long as value semantics remain externally observable.
When you create a class instance, a variable represents a reference. After assigning the reference to another variable, both point to the same instance; mutating a property through either reference is visible through the other. Swift uses Automatic Reference Counting (ARC) to manage class-instance lifetimes, while a structure value itself is not managed by reference-counting that value.
The two variables at the top own observably independent Score values.
The two constant references at the bottom point to one Player instance.
let fixes the binding: it prevents mutation or reassignment of a structure value and prevents a class reference from pointing elsewhere, but it does not stop the class instance’s var properties from changing.
A structure method cannot mutate self or its stored properties by default.
A method that does so must be marked mutating, and the structure binding at the call site must be a var.
A mutating method can even replace the whole self, so the marker expresses write access to the complete value.
Class instance methods need no mutating marker to change var properties because the reference keeps pointing to the same instance.
That convenience also means aliases can observe a change from far away.
When shared mutation is not part of the domain contract, return a new structure value or confine mutation to an explicit owner.
A structure receives a synthesized memberwise initializer when no conflicting custom initializer prevents synthesis. It supplies a parameter for each stored property, but its default access level is not a public API you should casually depend on; explicitly declare the construction contract of a public model. Classes do not receive a memberwise initializer, although every stored property must still have a value before initialization finishes.
Classes support inheritance, type casting, deinit, and reference identity; structures do not have these class-only capabilities.
Both can conform to protocols, so a need for polymorphism does not itself imply a need for a class.
Inheritance justifies a class only when a base implementation and overrides genuinely belong in the model.
The following comparison reduces the choice to testable contracts:
| Contract | Structure | Class |
|---|---|---|
| Assignment and passing | Creates a logically independent value | Copies a reference to the same instance |
| Mutating methods | Requires mutating to change self | Can change instance var properties directly |
| Identity | Has no === instance identity | Uses === or !== |
| Initialization | Can synthesize a memberwise initializer | Does not synthesize a memberwise initializer |
| Inheritance and teardown | Has no class inheritance or deinit | Supports inheritance and deinit |
| Typical role | Snapshots, records, independent state | Shared entities, lifetimes, and base-class hierarchies |
A structure can also contain a class reference. The outer structure is still copied, but the field copies only the reference, so two outer values can share one nested instance. This forms a shallow-copy boundary that the type design and tests must make explicit.
Examples
Independent values and shared instances after assignment
The first program puts the same score into structure variables and a class instance. Mutating the structure copy affects only that copy; mutating the instance through the second class reference changes what the first reference observes too. The final identity check directly confirms that both references point to the same instance.
// # not executed here: Swift toolchain is not installed.
struct Score {
var points: Int
}
final class Player {
let name: String
var score: Score
init(name: String, score: Score) {
self.name = name
self.score = score
}
}
var homeScore = Score(points: 10)
var copiedScore = homeScore
copiedScore.points += 5
let firstView = Player(name: "Mina", score: homeScore)
let secondView = firstView
secondView.score.points += 7
print("values:", homeScore.points, copiedScore.points)
print("references:", firstView.score.points, secondView.score.points)
print("same instance:", firstView === secondView)Not executed here: Swift toolchain is not installed.homeScore and copiedScore are two values, so writing to the latter does not feed back into the former.
firstView and secondView are two entrances to one Player.
Notice that placing Score in Player.score also copies the value, so the score inside the instance starts from 10, not the copy’s 15.
A memberwise initializer and a mutating method
Cart has no handwritten initializer, so the compiler synthesizes a memberwise initializer.
Because itemCount has a default, the caller needs to pass only owner; add(_:) changes the structure itself and must therefore be marked mutating.
The snapshot saved before the next mutation continues to represent the old state.
// # not executed here: Swift toolchain is not installed.
struct Cart {
let owner: String
private(set) var itemCount = 0
mutating func add(_ quantity: Int) {
precondition(quantity > 0)
itemCount += quantity
}
}
var cart = Cart(owner: "Mina")
cart.add(2)
let snapshot = cart
cart.add(3)
print("owner:", cart.owner)
print("snapshot items:", snapshot.itemCount)
print("current items:", cart.itemCount)Not executed here: Swift toolchain is not installed.Declaring cart with let would make the call to add(_:) a compile error.
The snapshot needs no freezing operation because it is already an independent value from that moment.
A public library should still consider declaring init(owner:) explicitly instead of treating the synthesized initializer as a stable external contract.
A reference inside a structure is not deep-copied
This example deliberately stores a mutable Counter class inside a Report structure.
After Report is copied, the title fields are independent, but both counter references still point to the same instance.
The outer value semantics do not automatically become deep independence for the complete object graph.
// # not executed here: Swift toolchain is not installed.
final class Counter {
var value: Int
init(value: Int) {
self.value = value
}
}
struct Report {
var title: String
var counter: Counter
}
var morning = Report(
title: "Morning",
counter: Counter(value: 1)
)
var evening = morning
evening.title = "Evening"
evening.counter.value += 1
print("titles:", morning.title, evening.title)
print("counts:", morning.counter.value, evening.counter.value)
print("shared counter:", morning.counter === evening.counter)Not executed here: Swift toolchain is not installed.Swift has not violated value semantics here: the value of Report.counter is itself a class reference.
When you need a complete snapshot, make the nested state a value too, or provide a clearly named copy operation that creates a new Counter.
Do not make generic “deep copy” the default answer; first define which resources are allowed to be shared.
Wrap value snapshots in shared identity
Many real models combine both semantics instead of choosing only one.
An editing session needs shared identity, so it is a class; document content represents state at a moment, so it is a structure.
Copying document before changing the session creates a snapshot unaffected by the later rename.
// # not executed here: Swift toolchain is not installed.
struct Document {
let id: Int
var title: String
}
final class EditorSession {
let sessionID: String
var document: Document
init(sessionID: String, document: Document) {
self.sessionID = sessionID
self.document = document
}
func renameDocument(to title: String) {
document.title = title
}
}
let session = EditorSession(
sessionID: "S-42",
document: Document(id: 7, title: "Draft")
)
let collaborator = session
let beforeRename = session.document
collaborator.renameDocument(to: "Reviewed")
print("before:", beforeRename.title)
print("current:", session.document.title)
print("shared session:", session === collaborator)Not executed here: Swift toolchain is not installed.The purpose of beforeRename is clear from its name and type: it is a value snapshot, not a second live view.
collaborator intentionally shares the session’s identity, so session observes a mutation made through it.
This combination limits sharing to the object that genuinely needs coordination.
Pitfalls
Copy-on-write and noncopyable structures
Copy-on-write (COW) is a storage optimization for implementing value semantics. Several logical values may temporarily share an internal class instance; when one value is about to mutate, it first copies the storage if that storage is not unique. Callers still observe two independent values, so COW changes the timing of physical copying, not the type’s public semantics.
Swift standard-library value types including Array, Dictionary, and String use strategies of this kind.
A custom large value can use the same pattern, but it must fully encapsulate its reference storage and check uniqueness on every mutation path.
Checking only some methods exposes shared mutation again through values that appear independent.
// # not executed here: Swift toolchain is not installed.
final class Storage {
var values: [Int]
init(_ values: [Int]) {
self.values = values
}
}
struct Buffer {
private var storage: Storage
init(_ values: [Int]) {
storage = Storage(values)
}
var values: [Int] {
storage.values
}
mutating func append(_ value: Int) {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(storage.values)
}
storage.values.append(value)
}
}
let original = Buffer([1, 2])
var revised = original
revised.append(3)
print("original:", original.values)
print("revised:", revised.values)Not executed here: Swift toolchain is not installed.isKnownUniquelyReferenced(_:) checks whether the passed class reference is currently known to be unique.
It is not a synchronization primitive or proof that there will always be one owner; an extra reference makes it return false, while concurrent access still needs a separate isolation design.
Encapsulation must prevent callers from reaching around append(_:) to mutate Storage directly.
Modern Swift also has one exception that the shorthand “structures copy” must acknowledge.
Structures and enumerations conform to Copyable by default, but they can explicitly opt out with ~Copyable to model linear resources such as file descriptors or unique tokens.
Such a declaration is still a value type, but ordinary assignment-is-copy teaching no longer applies; APIs must also say whether they borrow or consume that value.
Choosing a structure therefore chooses a value model first; it does not promise that every value can be copied without limit or that it occupies a particular memory location. For an ordinary copyable model, independent mutation is the key guarantee; for a noncopyable model, unique ownership constraints become part of the contract. Both should be expressed by the type interface rather than left for callers to infer from an implementation.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug