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.

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.

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:

Use reference semantics when:

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

8. Decision Guide

If you are deciding between a value type and a reference type, use this quick guide.

A useful mental model is this: choose a struct for data, choose a class for identity and intentional sharing.

9. Key Points

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.