Swift Identity Operators (===, !==): Compare Object References

Swift identity operators, === and !==, are used to check whether two variables refer to the exact same class instance in memory. This is different from checking whether two values merely contain the same data. In this tutorial, you will learn what identity means in Swift, how these operators work, when to use them, and how to avoid confusing identity with equality when working with reference types.

1. What Is Identity in Swift?

In Swift, identity answers a very specific question: do two references point to the same object instance? The identity operators are only for reference types, which means class instances.

Swift has both value types and reference types. A struct is copied when assigned, but a class instance is shared by reference. Identity operators exist because with reference types, two variables may refer to one shared object.

If you need to know whether two things contain the same data, use equality such as ==. If you need to know whether they are literally the same object, use ===.

2. Why Identity Operators Matter

Identity checks matter when your program stores, passes, and mutates class instances in multiple places. In these cases, two variables may show the same property values, but they may still be different objects. That difference affects updates, caching, shared state, and object tracking.

You should reach for identity operators when you need to confirm shared object references, such as checking whether a cached instance was reused, whether two properties point to the same model object, or whether an item in a graph structure is the exact node you expect.

You should not use identity operators when comparing value types or when your real goal is content comparison. For example, two separate User objects with the same ID and name may represent the same real-world user, but identity would still report them as different objects unless they are the same instance.

3. Basic Syntax or Core Idea

The core idea is simple: create a class, store an instance in one or more variables, and then compare those references with === or !==.

Minimal syntax

This example shows the smallest useful form of an identity comparison.

class Person {
    let name: String

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

let first = Person(name: "Maya")
let second = first
let third = Person(name: "Maya")

print(first === second)
print(first === third)
print(first !== third)

Here is what each part means:

This example shows the central rule: identity compares references, not data.

Identity works only with classes

If you try to use identity operators with value types like structs, Swift will reject the code because value types do not have reference identity in this sense.

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

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

// a === b  // Error: identity operators apply only to class instances

This is why identity is tied to reference semantics in Swift.

4. Step-by-Step Examples

Example 1: Two variables sharing one class instance

This first example demonstrates the most direct identity check. Both variables refer to the same object, so changing one reference affects the same underlying instance.

class Document {
    var title: String

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

let original = Document(title: "Report")
let sharedReference = original

sharedReference.title = "Updated Report"

print(original.title)
print(original === sharedReference)

The output shows that the title changed through both references, and original === sharedReference is true. That confirms both variables point to the same object.

Example 2: Same data, different instances

Now compare two separate objects that happen to store identical values. Their contents match, but their identities do not.

class Product {
    let id: Int
    let name: String

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

let p1 = Product(id: 101, name: "Keyboard")
let p2 = Product(id: 101, name: "Keyboard")

print(p1 === p2)
print(p1 !== p2)

Even though both products look the same, they are different instances. The first print is false and the second is true.

Example 3: Checking whether a cached object was reused

Identity operators are especially useful in caching. You may want to know whether a method returned the already stored instance or created a new one.

class ImageResource {
    let name: String

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

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

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

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

let cache = ImageCache()
let firstLoad = cache.loadImage(named: "logo")
let secondLoad = cache.loadImage(named: "logo")

print(firstLoad === secondLoad)

The result is true, which confirms the cache returned the same object instead of creating another one.

Example 4: Comparing optional class references

In real code, class references are often optional. Swift lets you compare optional class references with identity operators as long as the wrapped type is a class.

class Session {
    let token: String

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

let activeSession: Session? = Session(token: "abc123")
let sameSession = activeSession
let newSession: Session? = Session(token: "abc123")

print(activeSession === sameSession)
print(activeSession === newSession)

The first comparison is true because both optionals wrap the same object. The second is false because the instances are different.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Using identity when you really need equality

Many developers compare object references when they actually want to compare contents. That gives the wrong answer when separate objects have the same data.

Warning: Bad code: using identity to compare matching data.

class City {
    let name: String

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

let c1 = City(name: "Paris")
let c2 = City(name: "Paris")

print(c1 === c2)

This prints false because the objects are different instances, even though the names match.

The correct approach is to define equality based on the data you care about.

class City: Equatable {
    let name: String

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

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

let c1 = City(name: "Paris")
let c2 = City(name: "Paris")

print(c1 == c2)

This version compares values instead of object identity.

Mistake 2: Trying to use identity operators with structs

Identity only applies to class instances. Structs are value types, so identity operators are not valid.

Warning: Bad code: identity comparison on a struct.

struct User {
    let id: Int
}

let u1 = User(id: 1)
let u2 = User(id: 1)

// print(u1 === u2) // Error

The correct version compares values instead.

struct User: Equatable {
    let id: Int
}

let u1 = User(id: 1)
let u2 = User(id: 1)

print(u1 == u2)

This is the correct mental model: value types use equality, not identity.

Mistake 3: Assuming assignment creates a new class instance

With classes, assignment copies the reference, not the object. Developers sometimes expect a new independent instance and are surprised when identity is still true.

Warning: Bad assumption: assignment creates a separate object.

class Counter {
    var value: Int

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

let counter1 = Counter(value: 0)
let counter2 = counter1

counter2.value = 10
print(counter1.value)

This prints 10 because both variables refer to the same object.

If you need a separate instance, create one explicitly.

class Counter {
    var value: Int

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

let counter1 = Counter(value: 0)
let counter2 = Counter(value: counter1.value)

print(counter1 === counter2)

This prints false, showing the objects are distinct.

7. Best Practices

Use identity only for reference-semantics questions

Choose === when your question is about shared object instances, not matching content. This keeps your intent clear to other developers.

class Manager {
    let id: Int

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

let primaryManager = Manager(id: 7)
let currentManager = primaryManager

if currentManager === primaryManager {
    print("Using the exact same manager instance.")
}

This is a good use of identity because the code is checking whether both variables refer to the same manager object.

Define Equatable when data comparison matters

If your class needs content-based comparisons, implement == separately. That prevents misuse of identity operators in business logic.

class Book: Equatable {
    let isbn: String
    let title: String

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

    static func == (lhs: Book, rhs: Book) -> Bool {
        lhs.isbn == rhs.isbn
    }
}

let book1 = Book(isbn: "978-1", title: "Swift Basics")
let book2 = Book(isbn: "978-1", title: "Swift Basics")

print(book1 == book2)
print(book1 === book2)

This is best practice because the code distinguishes between equal content and identical instances.

Use identity in graph, cache, and shared-state logic

Identity is most valuable where object sharing is part of the design. In these situations, checking identity makes the program safer and easier to reason about.

class Node {
    let value: Int
    var next: Node?

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

let tail = Node(value: 3)
let head = Node(value: 1)
let middle = Node(value: 2)

head.next = middle
middle.next = tail

if middle.next === tail {
    print("Middle points to the exact tail node.")
}

This pattern is appropriate because linked structures depend on exact node relationships, not just matching values.

8. Limitations and Edge Cases

9. Practical Mini Project

Let us build a small cache-based profile loader. The goal is to show how identity operators can confirm whether repeated requests return the exact same user profile instance from a cache.

The full program below creates a UserProfile class and a ProfileStore that reuses instances for the same ID.

class UserProfile: Equatable {
    let id: Int
    var name: String

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }

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

class ProfileStore {
    private var cache: [Int: UserProfile] = [:]

    func profile(for id: Int, name: String) -> UserProfile {
        if let existingProfile = cache[id] {
            return existingProfile
        }

        let newProfile = UserProfile(id: id, name: name)
        cache[id] = newProfile
        return newProfile
    }
}

let store = ProfileStore()

let profileA = store.profile(for: 1, name: "Nina")
let profileB = store.profile(for: 1, name: "Nina")
let profileC = UserProfile(id: 1, name: "Nina")

print("A and B are the same instance:", profileA === profileB)
print("A and C are the same instance:", profileA === profileC)
print("A and C have equal data:", profileA == profileC)

profileB.name = "Nina Updated"
print("profileA name after updating profileB:", profileA.name)

This mini project demonstrates three important ideas. First, the cache returns the same object for the same user ID, so profileA === profileB is true. Second, a manually created profile with the same data is not the same instance, so profileA === profileC is false. Third, updating profileB also affects profileA because they refer to the same shared object.

10. Key Points

11. Practice Exercise

class Playlist {
    var title: String

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

let mainPlaylist = Playlist(title: "Morning Mix")
let backupReference = mainPlaylist
let copiedPlaylist = Playlist(title: "Morning Mix")

print(mainPlaylist === backupReference)
print(mainPlaylist === copiedPlaylist)

backupReference.title = "Evening Mix"
print(mainPlaylist.title)

12. Final Summary

Swift identity operators help you answer a narrow but important question: are these two references pointing to the exact same class object? That is why === and !== are only for class instances and not for value types like structs or enums. Once you understand the difference between identity and equality, many confusing behaviors around shared state become easier to reason about.

In this article, you learned the syntax of identity operators, saw how they behave with shared references and separate instances, explored common mistakes, and used identity in realistic patterns such as caching and object graphs. This topic connects directly to Swift's larger model of value semantics versus reference semantics, so mastering it will make your class-based code more predictable and safer to design.

As a next step, practice combining === with custom Equatable implementations so you can clearly separate object identity from business-level equality in your own Swift programs.