Swift Value Semantics vs Reference Semantics Explained
Understanding value semantics and reference semantics is one of the most important parts of learning Swift custom types. These rules affect how data is copied, how changes spread through your program, and why struct and class behave so differently. In this article, you will learn what each semantic model means, how Swift applies them, when to use each one, and how to avoid the bugs that come from choosing the wrong type.
Quick answer: In Swift, types with value semantics behave like independent copies, while types with reference semantics let multiple variables refer to the same shared instance. Most struct, enum, and standard library collection types are value-oriented, while class instances use reference semantics.
Difficulty: Beginner
Helpful to know first: You'll understand this better if you know basic Swift syntax, variables and constants, functions, and the difference between struct and class.
1. What Are the Options?
This comparison is really about two different ways a type can behave when you assign it, pass it to a function, or store it in another variable.
- Value semantics: the data acts like a separate value. When you assign it or pass it around, each place works with its own logical copy.
- Reference semantics: the data acts like a shared object. Multiple variables can point to the same instance.
- In Swift, struct and enum are typically used for value semantics.
- In Swift, class uses reference semantics.
- This matters because it changes how mutation works and whether state is isolated or shared.
Developers often feel this difference when one variable changes and another variable unexpectedly changes too. That usually means reference semantics are involved.
2. Side-by-Side Comparison
| Behavior | Value Semantics | Reference Semantics |
|---|---|---|
| Typical Swift type | struct, enum | class |
| Assignment | Creates an independent value | Copies the reference to the same instance |
| Mutation | Changes affect only that value | Changes can be seen through all references |
| Shared state | Usually avoided by default | Common and intentional |
| Equality checks | Usually value comparison with == | May use == for value equality and === for identity |
| Reasoning about code | Often simpler and safer | Useful for shared mutable state and identity |
3. Key Differences Explained
Assignment behavior
With value semantics, assigning one variable to another gives the new variable its own value. Mutating the new variable does not change the original.
With reference semantics, assigning one variable to another does not create a new object by default. It creates another reference to the same instance.
Mutation and isolation
Value semantics make local changes easier to understand because data is isolated. If a function receives a value type, it works with that value unless you explicitly use inout or return a new result.
Reference semantics allow shared mutable state. That can be useful, but it also means changes in one part of the program may affect another part unexpectedly.
Identity
Value types usually care about what the data is. Reference types often care about both what the data contains and which exact instance it is.
That is why classes can be checked with === to ask whether two variables refer to the same instance.
Performance model
Beginners sometimes assume value semantics always means expensive copying. In Swift, that is not necessarily true. Standard library types like Array, String, and Dictionary use value semantics while often relying on copy-on-write internally.
Copy-on-write means Swift can delay making a real separate storage copy until one of the values is mutated. You still get value-style behavior, but often with efficient memory use.
Swift == vs ===
This comparison often appears alongside value vs reference semantics.
- == asks whether two values are equal in content, if the type supports equality.
- === asks whether two class references point to the exact same instance.
You use === only with class instances, not with structs.
4. When to Use Each
Neither approach is universally better. The right choice depends on the kind of data you are modeling.
Use value semantics when:
- The type represents data, such as a point, range, user settings, or configuration.
- You want changes to stay local and predictable.
- You want safer code with fewer shared-state bugs.
- The type should behave like an independent value when copied.
Use reference semantics when:
- The type has identity, such as a shared manager, cache, or object that must be observed from multiple places.
- Multiple parts of the program must refer to the same mutable instance.
- You need class-only features such as inheritance or weak references.
- Sharing state is intentional and part of the design.
A good Swift default is to start with struct and use class only when identity or shared mutable state is truly needed.
5. Code Examples
The examples below show how the same kind of data behaves differently depending on whether you model it as a struct or a class.
Example 1: Struct assignment creates an independent value
Here, Score is a value type. After assignment, each variable behaves independently.
struct Score {
var points: Int
}
var firstScore = Score(points: 10)
var secondScore = firstScore
secondScore.points = 50
print(firstScore.points)
print(secondScore.points)
This prints 10 and 50. Changing secondScore does not affect firstScore.
Example 2: Class assignment shares the same instance
Now the same idea is modeled with a class. Assignment copies the reference, not the underlying object.
class ScoreBoard {
var points: Int
init(points: Int) {
self.points = points
}
}
let boardA = ScoreBoard(points: 10)
let boardB = boardA
boardB.points = 50
print(boardA.points)
print(boardB.points)
This prints 50 twice because both variables refer to the same instance.
Example 3: Passing a struct into a function
Passing a value type into a function gives the function its own copy to work with.
struct UserSettings {
var volume: Int
}
func increaseVolume(settings: UserSettings) {
var copy = settings
copy.volume += 10
print(copy.volume)
}
let settings = UserSettings(volume: 20)
increaseVolume(settings: settings)
print(settings.volume)
The original settings value remains unchanged. The function changed only its local copy.
Example 4: Checking class identity with ===
This example shows the difference between two references that point to the same object and two separate instances with the same content.
class Document {
var title: String
init(title: String) {
self.title = title
}
}
let doc1 = Document(title: "Plan")
let doc2 = doc1
let doc3 = Document(title: "Plan")
print(doc1 === doc2)
print(doc1 === doc3)
This prints true and false. doc1 and doc2 are the same instance, but doc3 is a different one.
6. Common Mistakes
Mistake 1: Expecting a class assignment to make a separate copy
Many beginners assume assigning a class instance to a new variable duplicates the object. In Swift, it duplicates only the reference.
Problem: This code mutates shared state, so changing one variable also changes the other variable unexpectedly.
class Profile {
var name: String
init(name: String) {
self.name = name
}
}
let original = Profile(name: "Ava")
let copy = original
copy.name = "Liam"
print(original.name)
Fix: Use a struct when you want independent copies, or implement an explicit copy operation for the class.
struct Profile {
var name: String
}
var original = Profile(name: "Ava")
var copy = original
copy.name = "Liam"
print(original.name)
The corrected version works because a struct assignment gives each variable its own value.
Mistake 2: Trying to use === with a struct
The identity operator checks whether two class references point to the same instance. Structs do not have reference identity in that sense.
Problem: This code attempts an identity comparison on a value type, which causes a compile-time error because === is only for class instances.
struct Point {
var x: Int
var y: Int
}
let a = Point(x: 1, y: 2)
let b = a
print(a === b)
Fix: Compare structs by value, usually with ==, after making the type conform to Equatable.
struct Point: Equatable {
var x: Int
var y: Int
}
let a = Point(x: 1, y: 2)
let b = a
print(a == b)
The corrected version works because value types are compared by their contents, not by object identity.
Mistake 3: Thinking a let class instance is fully immutable
A constant reference means the variable cannot point to a different instance later, but the instance's mutable properties can still change if they are declared with var.
Problem: This assumption leads to confusing bugs because the class instance can still be mutated through a constant reference.
class Counter {
var value: Int = 0
}
let counter = Counter()
counter.value = 5
Fix: Remember that let freezes the reference, not the internal mutable state of a class. If you need stronger immutability, use a struct with immutable properties or design the class carefully.
struct Counter {
let value: Int
}
let counter = Counter(value: 5)
The corrected version works because the struct's stored property is immutable and the value is not shared through references.
Mistake 4: Expecting a function to mutate a struct argument automatically
Passing a struct into a function does not let the function change the caller's original value unless you explicitly use inout or return a new value.
Problem: This code mutates only a local copy, so the original value outside the function stays unchanged.
struct Balance {
var amount: Int
}
func addFunds(balance: Balance) {
var balance = balance
balance.amount += 100
}
var wallet = Balance(amount: 50)
addFunds(balance: wallet)
print(wallet.amount)
Fix: Use inout when the function should mutate the caller's value directly.
struct Balance {
var amount: Int
}
func addFunds(balance: inout Balance) {
balance.amount += 100
}
var wallet = Balance(amount: 50)
addFunds(balance: &wallet)
print(wallet.amount)
The corrected version works because inout allows the function to modify the caller's original value.
7. Tradeoffs and Edge Cases
- Copy-on-write can hide implementation details: types like Array feel like values even though Swift may optimize storage behind the scenes.
- Reference semantics can simplify shared models: if several objects truly need the same mutable state, a class may be the right design.
- Value semantics can reduce bugs: isolated data is easier to test and reason about, especially in larger codebases.
- Classes are not always more efficient: shared references avoid some copying, but shared mutable state can introduce complexity, synchronization concerns, and surprising side effects.
- Structs can contain references: a struct itself is a value type, but if it stores a class instance, some of its behavior may still involve shared reference state internally.
- Mutating methods matter for structs: methods that change stored properties in a struct must be marked mutating.
- Identity-based logic is class-specific: if your design depends on whether two variables point to the same exact object, a value type is probably not the right tool.
8. Decision Guide
If you are deciding between a value type and a reference type, use this quick guide.
- If the type mainly represents data, use struct.
- If copies should be independent, use struct.
- If multiple parts of the app must share and mutate the same instance, use class.
- If identity matters, such as “this exact object,” use class.
- If you are unsure, start with struct and switch to class only when the design clearly needs reference semantics.
A useful mental model is this: choose a struct for data, choose a class for identity and intentional sharing.
9. Key Points
- Value semantics give each assignment its own independent logical value.
- Reference semantics let multiple variables point to the same class instance.
- struct and enum are the usual tools for value semantics in Swift.
- class uses reference semantics and supports identity checks with ===.
- == compares equality of content, while === compares object identity.
- Copy-on-write lets many standard library value types stay efficient.
- Starting with value types usually leads to safer and more predictable code.
10. Final Summary
Swift value semantics and reference semantics describe how data behaves when it is assigned, passed around, and mutated. Value types such as structs usually give you independent, predictable data, while reference types such as classes let multiple variables share the same underlying instance. That difference affects everything from simple assignments to larger architectural decisions.
In practice, this topic is really about control over change. If you want isolated state and easier reasoning, value semantics are usually the better default. If you need identity and shared mutable behavior, reference semantics may be the right choice. A strong next step is to study struct vs class in more detail, then explore copy-on-write and Equatable to deepen your understanding of how Swift models data.