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.
- === checks whether two references point to the same class instance.
- !== checks whether two references point to different class instances.
- Identity works only with reference types, which in Swift means classes.
- Identity is not the same as equality. Equality usually means “same content,” while identity means “same instance.”
- This topic is commonly confused with ==, which compares values for equality when a type supports it.
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:
- Checking whether two variables refer to the same model object.
- Avoiding duplicate work when caching class instances.
- Testing whether an object was replaced or reused.
- Comparing parent, child, or linked objects in object graphs.
- Debugging unexpected shared state in class-based code.
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
- Checking whether two variables refer to the same shared manager or service object.
- Confirming that a cache or dependency container returns a reused class instance.
- Comparing nodes in tree or graph structures built with classes.
- Making sure an object in a collection is the exact object you expect, not just one with matching data.
- Testing object replacement during state updates, reloading, or lifecycle changes.
- Debugging bugs caused by accidental shared mutable state across multiple references.
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
- === and !== work only with classes. They do not apply to structs, enums, tuples, or built-in value types like Int and String.
- Two distinct objects can be equal in content but still fail an identity check.
- Assigning a class instance to another variable does not clone it; both names refer to the same object.
- If a class conforms to Equatable, that does not change how identity works. Equality and identity remain separate ideas.
- Optional class references can be compared with identity operators when the underlying types are class instances, but you still need the variables to be compatible reference types.
- If identity checks seem “not working,” the most common cause is comparing two separately created objects that merely contain the same data.
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
- === checks whether two references point to the same class instance.
- !== checks whether two references point to different class instances.
- Identity is not the same as equality.
- Use == for matching values and === for matching objects.
- Identity operators work with classes, not with structs or enums.
- Assigning a class instance to another variable shares the same object rather than copying it.
11. Practice Exercise
Build a small example with a Book class and verify when identity is true or false.
- Create a Book class with a title property.
- Create one book instance and assign it to a second variable.
- Create a third book instance with the same title.
- Print whether the first and second references have the same identity.
- Print whether the first and third references have the same identity.
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.