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.
- === returns true when two references point to the same class instance.
- !== returns true when two references point to different class instances.
- Identity is not the same as value equality.
- Identity operators do not work with structs, enums, strings, or integers.
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:
- first stores a new Person object.
- second = first does not create a copy. Both references point to the same object.
- third is a new object with the same property value.
- first === second is true because they are the same instance.
- first === third is false because they are different instances.
- first !== third is true for the same reason.
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
- Checking whether a cache returned the exact same class instance that was stored earlier.
- Verifying that two properties in an object graph point to the same shared node.
- Preventing duplicate processing when the same class instance appears in multiple collections.
- Testing whether a delegate, manager, or controller reference is the exact object expected.
- Detecting whether a factory or singleton-style provider is reusing an existing object.
- Comparing parent and child references in tree or graph structures where instance identity matters more than matching values.
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
- Identity operators work only with class instances and optional class references, not with structs, enums, tuples, or primitive values.
- Two different objects can hold exactly the same property values and still fail an identity comparison.
- Assignment of a class instance creates another reference to the same object, not a cloned object.
- If your class conforms to Equatable, == and === can legitimately produce different results.
- Identity says nothing about whether two objects should be considered logically equivalent in your business model.
- When working with optionals, identity comparisons are valid only if the wrapped type is a class type.
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
- === checks whether two references point to the same class instance.
- !== checks whether two references point to different class instances.
- Identity is different from equality and should not be used for content comparison.
- Identity operators work only with reference types such as classes.
- Assigning a class instance to another variable copies the reference, not the object.
- Separate objects can contain identical data and still fail an identity comparison.
- Identity is especially useful in caches, graphs, delegates, and shared-state systems.
11. Practice Exercise
- Create a class named Playlist with a mutable title property.
- Create one instance named mainPlaylist.
- Assign it to another variable named backupReference.
- Create a separate instance named copiedPlaylist with the same title value.
- Print whether mainPlaylist and backupReference are the same instance.
- Print whether mainPlaylist and copiedPlaylist are the same instance.
- Change the title through backupReference and print mainPlaylist.title.
- Expected output: the first identity check should be true, the second should be false, and the final printed title should reflect the updated value.
- Hint: remember that classes are reference types, so assignment shares the same object.
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.