Blog / August 10, 2023 / 5 mins read / By Mahi Garg

Strong vs Weak vs Unowned References in Swift

In the world of Swift programming, memory management is a critical aspect that developers need to consider to create efficient and reliable apps. One of the core concepts in memory management is the use of references to objects, which determine how objects are retained and released in memory. In Swift, there are three types of references that play a crucial role: strong, weak, and unowned references. In this blog, we will explore these three types of references with clear examples to help you understand their significance.

Strong References

A strong reference is the default type of reference in Swift. When you create a new instance of a class and assign it to a property, a strong reference is established. Strong references increase the retain count of an object, ensuring that it remains in memory as long as there are strong references pointing to it. Let’s consider an example involving two classes that reference each other:

class Author {
    let name: String
    var book: Book?
    
    init(name: String) {
        self.name = name
        print("\(name) is an author.")
    }
    
    deinit {
        print("\(name) is no longer an author.")
    }
}

class Book {
    let title: String
    var author: Author?
    
    init(title: String) {
        self.title = title
        print("\(title) is a book.")
    }
    
    deinit {
        print("\(title) is no longer a book.")
    }
}

var author: Author?
var book: Book?

author = Author(name: "J.K. Rowling")
book = Book(title: "Harry Potter and the Sorcerer's Stone")

author?.book = book
book?.author = author

author = nil // Neither Author nor Book instance is deallocated
book = nil

The primary difference with strong references is that they keep the referenced object in memory as long as there is at least one strong reference to it. In the example above, even though both author and book are set to nil, neither instance is deallocated because the strong references maintain a reference cycle between them.

Weak References

Weak references are used to avoid strong reference cycles, also known as retain cycles, which can lead to memory leaks. When you declare a reference as weak, it doesn’t increase the retain count of the object. If the object it points to is deallocated, the weak reference automatically becomes nil. Let’s see this in action:

class Customer {
    let name: String
    weak var creditCard: CreditCard?
    
    init(name: String) {
        self.name = name
        print("\(name) is a customer.")
    }
    
    deinit {
        print("\(name) is no longer a customer.")
    }
}

class CreditCard {
    let number: String
    var owner: Customer?
    
    init(number: String) {
        self.number = number
        print("\(number) is a credit card.")
    }
    
    deinit {
        print("\(number) is no longer a credit card.")
    }
}

var customer: Customer?
var card: CreditCard?

customer = Customer(name: "Alice")
card = CreditCard(number: "1234-5678-9012-3456")

customer?.creditCard = card
card?.owner = customer

customer = nil // Customer instance is deallocated
// The CreditCard's owner weak reference becomes nil automatically
card = nil

The key distinction of weak references is that they do not prevent the object they reference from being deallocated. In the example above, when the customer instance is deallocated, the weak reference in the card instance becomes automatically nil, preventing a retain cycle.

Unowned References

Unowned references are similar to weak references in that they also avoid retain cycles. However, unlike weak references, unowned references assume that the referenced object will always exist as long as the unowned reference exists. If the referenced object is deallocated, accessing the unowned reference will result in a runtime error. Here’s an example to illustrate this:

class Teacher {
    let name: String
    unowned var school: School
    
    init(name: String, school: School) {
        self.name = name
        self.school = school
        print("\(name) is a teacher.")
    }
    
    deinit {
        print("\(name) is no longer a teacher.")
    }
}

class School {
    let name: String
    var teacher: Teacher?
    
    init(name: String) {
        self.name = name
        print("\(name) is a school.")
    }
    
    deinit {
        print("\(name) is no longer a school.")
    }
}

var school: School?
var teacher: Teacher?

school = School(name: "Swift High School")
teacher = Teacher(name: "John Smith", school: school!)

school = nil // School instance is deallocated
// Accessing teacher.school will result in a runtime error
teacher = nil

Unowned references provide a balance between strong and weak references. They don’t increase the retain count like strong references, yet they require the referenced object to be valid during the entire lifetime of the unowned reference. In the example above, if the school instance is deallocated, accessing teacher.school will lead to a runtime error.

Differences Between Strong, Weak, and Unowned References

Strong References:
  • Keep the referenced object in memory as long as at least one strong reference exists.
  • Setting a strong reference to nil does not immediately deallocate the object.
Weak References:
  • Do not prevent the referenced object from being deallocated.
  • Automatically become nil when the referenced object is deallocated.
  • Used to avoid retain cycles and potential memory leaks.
Unowned References:
  • Do not increase the retain count of the referenced object.
  • Assume the referenced object’s existence during the lifetime of the unowned reference.
  • Accessing an unowned reference after the referenced object is deallocated leads to a runtime error.

Conclusion

Understanding the differences between strong, weak, and unowned references is crucial for effective memory management in Swift. Strong references keep objects in memory as long as they are referenced, weak references prevent retain cycles and become nil when the referenced object is deallocated, and unowned references assume the referenced object’s existence and lead to runtime errors if accessed after deallocation. By choosing the appropriate reference type based on the requirements of your code, developers can create stable and memory-efficient Swift applications. Happy coding!

Comments