Swift Reference Identity (===): Compare Class Instances Safely

Swift reference identity lets you check whether two variables point to the exact same class instance in memory. This matters when you work with reference types and need to know whether two values are merely equal in content or are literally the same object.

Quick answer: In Swift, === returns true when two class references point to the same instance, and !== returns true when they point to different instances. Use these operators only with class instances, not with structs, enums, or basic value types.

Difficulty: Beginner

Helpful to know first: You'll understand this better if you know basic Swift syntax, the difference between classes and structs, and how variables store values or references.

1. What Is Reference Identity?

Reference identity is Swift's way of asking, “Are these two names referring to the exact same class object?” This is different from asking whether two values merely contain equal data.

Think of it this way: two separate notebooks can contain the same words, but they are still different physical notebooks. Equality asks whether the contents match. Identity asks whether they are the very same notebook.

2. Why Reference Identity Matters

Reference identity matters whenever your program keeps track of shared objects. If multiple variables point to the same class instance, changing that instance through one variable affects what the others see.

Common situations where identity matters include:

If you use == when you really need ===, you may accidentally compare content instead of checking whether the object itself is shared. If you use === when you really need value comparison, you may reject objects that have identical data but were created separately.

3. Basic Syntax or Core Idea

The core syntax is short, but the idea behind it is important. Identity operators compare references to class instances.

Basic identity syntax

Here is the smallest useful example:

class User {
    let name: String

    init(name: String) {
        self.name = name
    }
}

let firstUser = User(name: "Maya")
let secondUser = firstUser

print(firstUser === secondUser) // true

secondUser is not a new object. It is another reference to the same User instance, so === returns true.

Different instances with the same data

Now compare two separate objects that happen to hold the same property values:

let userA = User(name: "Maya")
let userB = User(name: "Maya")

print(userA === userB) // false

These instances contain the same name, but they are different objects, so they do not have the same identity.

Identity vs equality

To see the difference clearly, define equality separately:

class Account: Equatable {
    let id: Int

    init(id: Int) {
        self.id = id
    }

    static func == (lhs: Account, rhs: Account) -> Bool {
        lhs.id == rhs.id
    }
}

let account1 = Account(id: 100)
let account2 = Account(id: 100)
let account3 = account1

print(account1 == account2)  // true
print(account1 === account2) // false
print(account1 === account3) // true

This shows the key difference: == can mean equal content, while === means same instance.

4. Step-by-Step Examples

The best way to understand identity is to see how references behave in realistic code.

Example 1: Assigning one class instance to another variable

When you assign a class instance to a new variable, both variables refer to the same object.

class Document {
    var title: String

    init(title: String) {
        self.title = title
    }
}

let original = Document(title: "Draft")
let alias = original

alias.title = "Final Draft"

print(original.title)         // Final Draft
print(original === alias) // true

Because both variables share the same instance, changing one is visible through the other.

Example 2: Creating two separate instances

Two objects can look the same while still being different instances.

let doc1 = Document(title: "Report")
let doc2 = Document(title: "Report")

print(doc1.title == doc2.title) // true
print(doc1 === doc2)         // false

The titles match, but the objects are separate. Identity correctly reports that they are not the same instance.

Example 3: Checking whether a cached instance is reused

Identity is useful when a cache should return the exact same object instead of building a new one.

class ImageResource {
    let filename: String

    init(filename: String) {
        self.filename = filename
    }
}

class ImageCache {
    private var storage: [String: ImageResource] = [:]

    func image(named filename: String) -> ImageResource {
        if let cached = storage[filename] {
            return cached
        }

        let newImage = ImageResource(filename: filename)
        storage[filename] = newImage
        return newImage
    }
}

let cache = ImageCache()
let image1 = cache.image(named: "logo.png")
let image2 = cache.image(named: "logo.png")

print(image1 === image2) // true

This confirms that the cache is reusing the same instance rather than creating duplicates.

Example 4: Detecting whether an object was replaced

Sometimes you need to know whether a property still refers to the old object or a newly assigned one.

class Session {
    let token: String

    init(token: String) {
        self.token = token
    }
}

var currentSession = Session(token: "abc123")
let savedReference = currentSession

currentSession = Session(token: "xyz999")

print(savedReference === currentSession) // false

The stored reference still points to the old session object, while currentSession now points to a new one.

5. Practical Use Cases

6. Common Mistakes

Reference identity is simple once you know what it does, but beginners often run into a few predictable mistakes.

Mistake 1: Using === when you actually need ==

This happens when you want to compare the data inside two objects, but you use identity instead of equality.

Problem: This code checks whether two instances are the same object, not whether they store the same content. Two separate instances with identical data will still produce false.

class Person {
    let name: String

    init(name: String) {
        self.name = name
    }
}

let person1 = Person(name: "Lina")
let person2 = Person(name: "Lina")

print(person1 === person2) // false

Fix: If you want to compare content, make the class conform to Equatable and use ==.

class Person: Equatable {
    let name: String

    init(name: String) {
        self.name = name
    }

    static func == (lhs: Person, rhs: Person) -> Bool {
        lhs.name == rhs.name
    }
}

let person1 = Person(name: "Lina")
let person2 = Person(name: "Lina")

print(person1 == person2) // true

The corrected version works because equality compares the chosen properties instead of object identity.

Mistake 2: Trying to use === with a struct or other value type

Identity operators only work with class instances. Structs and enums are value types, so they do not have reference identity in this sense.

Problem: This code attempts an identity comparison on a value type, which causes a compile-time error because === requires class references.

struct Point {
    let x: Int
    let y: Int
}

let a = Point(x: 1, y: 2)
let b = Point(x: 1, y: 2)

print(a === b)

Fix: Use == for value types that conform to Equatable, or compare individual properties.

struct Point: Equatable {
    let x: Int
    let y: Int
}

let a = Point(x: 1, y: 2)
let b = Point(x: 1, y: 2)

print(a == b) // true

The corrected version works because value types are compared by value, not by reference identity.

Mistake 3: Assuming a copied reference creates a new object

Assigning a class instance to another variable does not clone it. It only creates another reference to the same instance.

Problem: This code assumes backup is an independent copy, but both variables refer to the same object. Mutating one changes the shared instance.

class Settings {
    var theme = "Light"
}

let current = Settings()
let backup = current

backup.theme = "Dark"
print(current.theme) // Dark

Fix: Create a new instance manually if you need an independent object with copied data.

class Settings {
    var theme: String

    init(theme: String) {
        self.theme = theme
    }
}

let current = Settings(theme: "Light")
let backup = Settings(theme: current.theme)

backup.theme = "Dark"
print(current.theme) // Light
print(current === backup) // false

The corrected version works because it creates a separate instance rather than sharing the original object.

7. Best Practices

Use identity operators intentionally. They solve a specific problem and are most helpful when you want to reason about shared objects.

Practice 1: Use === only for class-instance identity checks

A less helpful approach is to reach for === whenever two things should “match.” That usually leads to confusion. Prefer it only when the exact same object matters.

class Task {
    let id: Int

    init(id: Int) {
        self.id = id
    }
}

let task = Task(id: 1)
let sameTask = task

print(task === sameTask) // true

This keeps identity checks meaningful and prevents accidental misuse as a content comparison tool.

Practice 2: Define Equatable when content comparison is important

If your class objects need to be compared by their data, make that rule explicit with Equatable. Do not rely on identity for business logic that really depends on matching values.

class Product: Equatable {
    let sku: String

    init(sku: String) {
        self.sku = sku
    }

    static func == (lhs: Product, rhs: Product) -> Bool {
        lhs.sku == rhs.sku
    }
}

This makes your comparison rules clearer to both the compiler and other developers.

Practice 3: Use identity to debug shared mutable state

When unexpected changes appear in multiple places, identity checks can quickly reveal whether the same object is being shared.

class Profile {
    var nickname: String

    init(nickname: String) {
        self.nickname = nickname
    }
}

let mainProfile = Profile(nickname: "neo")
let editorProfile = mainProfile

print(mainProfile === editorProfile) // true

This is a fast way to confirm whether two parts of your program are working with the same instance.

8. Limitations and Edge Cases

A useful rule of thumb is this: if you care about the object itself, use identity. If you care about what the object stores, use equality.

9. Practical Mini Project

This mini project simulates a simple user session store. It shows how identity helps confirm whether the currently loaded session is still the same object or a replacement.

class UserSession {
    let userID: Int
    var status: String

    init(userID: Int, status: String) {
        self.userID = userID
        self.status = status
    }
}

class SessionStore {
    private var currentSession: UserSession?

    func login(userID: Int) -> UserSession {
        let session = UserSession(userID: userID, status: "Active")
        currentSession = session
        return session
    }

    func session() -> UserSession? {
        currentSession
    }

    func refreshSession() {
        if let existing = currentSession {
            currentSession = UserSession(userID: existing.userID, status: "Refreshed")
        }
    }
}

let store = SessionStore()
let firstSession = store.login(userID: 42)

if let loadedSession = store.session() {
    print(firstSession === loadedSession) // true
}

store.refreshSession()

if let newSession = store.session() {
    print(firstSession === newSession) // false
    print(newSession.status) // Refreshed
}

This example shows both sides of identity. The first lookup returns the same instance, so identity is true. After refresh, the store replaces that session with a new object, so identity becomes false.

10. Key Points

11. Practice Exercise

Build a small example with a Book class and verify when identity is true or false.

Expected output: The first comparison should be true. The second should be false.

Hint: Identity becomes true only when two variables refer to the exact same class instance.

class Book {
    let title: String

    init(title: String) {
        self.title = title
    }
}

let book1 = Book(title: "Swift Guide")
let book2 = book1
let book3 = Book(title: "Swift Guide")

print(book1 === book2) // true
print(book1 === book3) // false

12. Final Summary

Swift reference identity answers a very specific question: are these two references pointing to the same class instance? You check that with === and !==. This is different from equality, which is about whether two values or objects should be considered equal in content.

The most important habit is choosing the right kind of comparison. Use === when shared object identity matters, such as caches, reused instances, and debugging reference behavior. Use == when you want to compare data. A strong next step is to study Swift classes versus structs, since that difference explains why identity exists in the first place.