A lazy var is a property whose initial value is not calculated until the first time it’s called. It’s part of a family of properties in which we have constant properties, computed properties, and mutable properties.
A lazy property might be lesser known to beginners in Swift but are actually super valuable once you know when and how to use them. There are a few important things to learn so you know when to use which type of property.
A lazy stored property is a property whose initial value is not calculated until the first time it is used. You indicate a lazy stored property by writing the lazy
modifier before its declaration.
What is a lazy var?
The word ‘lazy’ describes best what a lazy property means as the initial value of a lazy property is not calculated until the first time it’s used. In other words: It’s lazy initialization. This could be a great way to optimize your code to prevent doing any unnecessary work.
A lazy var looks as follows:
lazy var oldest: Person? = {
return people.max(by: { $0.age < $1.age })
}()
When should I use a lazy stored property?
A lazy var can be a great solution for code that’s expensive and unlikely to be called consistently. Let’s take the above example and use it inside a view model:
struct Person {
let name: String
let age: Int
}
struct PeopleViewModel {
let people: [Person]
lazy var oldest: Person? = {
print("Lazy var oldest initialized")
return people.max(by: { $0.age < $1.age })
}()
init(people: [Person]) {
self.people = people
print("View model initialized")
}
}
Sorting a collection of elements can be expensive so we want to make sure we’re only performing this operation if we’re actually using the value. Therefore, a lazy var is a great solution. The print statement is getting called once the value is actually calculated:
var viewModel = PeopleViewModel(people: [
Person(name: "Antoine", age: 30),
Person(name: "Jaap", age: 3),
Person(name: "Lady", age: 3),
Person(name: "Maaike", age: 27)
])
// Prints: "View model initialized"
print(viewModel.oldest)
// Prints: "Lazy var oldest initialized"
// Prints: Person(name: "Antoine", age: 30)
As you can see, the print statement is not called upon initializing the view model itself but only after calling the property oldest
.
The difference between a computed and a lazy stored property
It’s important to understand the difference between a computed property and a lazy stored property. If we would’ve used a computed property, the value oldest
would be recalculated every time again:
struct PeopleViewModel {
let people: [Person]
var oldest: Person? {
print("oldest person calculated")
return people.max(by: { $0.age < $1.age })
}
}
print(viewModel.oldest)
// Prints: "oldest person calculated"
// Prints: Person(name: "Antoine", age: 30)
print(viewModel.oldest)
// Prints: "oldest person calculated"
// Prints: Person(name: "Antoine", age: 30)
This is a loss of performance compared to a lazy variable which is only called once:
struct PeopleViewModel {
let people: [Person]
lazy var oldest: Person? = {
print("oldest person calculated")
return people.max(by: { $0.age < $1.age })
}()
}
print(viewModel.oldest)
// Prints: "oldest person calculated"
// Prints: Person(name: "Antoine", age: 30)
print(viewModel.oldest)
// Prints: Person(name: "Antoine", age: 30)
Because of this difference it’s important to understand the lifecycle of lazy variables as it could introduce unexpected side effects.
Understanding the lifecycle of lazy variables
As lazy properties are only calculated once called it means it will also use the state of the moment it’s getting called. For example, if the collection of people is mutable it could mean that oldest is different when called before a mutating takes place:
struct PeopleViewModel {
var people: [Person]
lazy var oldest: Person? = {
print("oldest person calculated")
return people.max(by: { $0.age < $1.age })
}()
}
var viewModel = PeopleViewModel(people: [
Person(name: "Antoine", age: 30),
Person(name: "Jaap", age: 3),
Person(name: "Lady", age: 3),
Person(name: "Maaike", age: 27)
])
print(viewModel.oldest)
// Prints: "oldest person calculated"
// Prints: Person(name: "Antoine", age: 30)
viewModel.people.append(Person(name: "Jan", age: 69))
print(viewModel.oldest)
// Prints: Person(name: "Antoine", age: 30)
After adding Jan with an age of 69 you would expect the old person to return Jan. However, as oldest
is already initialised before the mutation took place, the stored value is set to Antoine with an age of 30. In this case, it might be better to go with a computed property instead as it takes into account the actual state of the people collection.
Lazy stored properties are mutable
Another thing to realize is the fact that lazy variables are mutable. This means you can only call lazy variables on mutable structs:
If your struct isn’t mutable and you’re calling the lazy variable, you’ll run into an error like:
Cannot use mutating getter on immutable value: ‘viewModel’ is a ‘let’ constant
This is only the case for value types, in this case, a struct. If our view model would’ve been a class, this error wouldn’t occur. If you want to learn more, you can read my post about the difference between classes and structs.
Conclusion
Lazy variables allow you to delay the initialisation of stored properties. This can be useful to only perform expensive work when it’s actually needed. The different between lazy- and computed properties is important in cases you need to have calculations based on the current state of values.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!