What are computed properties in Swift and when should you use them?
Published on: March 9, 2020One of Swift's incredibly useful features is its ability to dynamically compute the value of a property through a computed property. While this is a super handy feature, it can also be a source of confusion for newcomers to the language. A computed property can look a bit strange if you haven't seen one before; especially when you are learning about custom get
and set
closures for properties at the same time.
In this week's post, I would like to take some time to explain computed properties in-depth so you can begin using them in your codebase with confidence.
By the end of this post, you will have a solid understanding of how computed properties work, how and when they are executed and how you can begin using them in your code.
Understanding how a computed property works
A computed property in Swift is a property that can't be assigned a value directly. It calculates its current value based on a closure-like block of code that you return a value from. Let's look at a very basic example of a computed property:
struct User {
var firstName: String
var lastName: String
var fullName: String {
"\(firstName) \(lastName)"
}
}
In this example, I have defined a fullName
property on a struct that concatenates two strings as a computed property. Every time I access fullName
, the code between the {
and }
is executed to determine what the value of fullName
should be:
var user = User(firstName: "Donny", lastName: "Wals")
print(user.fullName) // "Donny Wals"
user.lastName = "Secret"
print(user.fullName) // "Donny Secret"
This is incredibly useful because the user's fullName
will always reflect the combination of the user's current firstName
and lastName
properties.
Because a computed property isn't a stored property, it's also okay to declare a computed property in an extension of an object:
struct User {
var firstName: String
var lastName: String
}
extension User {
var fullName: String {
"\(firstName) \(lastName)"
}
}
A computed property in Swift is a get-only property because it does not have any storage that you can assign a value to. This means that the following code where we attempt to assign a value to one of our computed properties is not valid Swift:
var user = User(firstName: "Donny", lastName: "Wals")
user.fullName = "New Name" // Cannot assign to property: 'fullName' is a get-only property
In my example, I was able to write my computed property in a single statement so I omitted the return
keyword. A computed property essentially follows the same rules as functions and closures do, so you can also have multiple lines in a computed property:
extension User {
var fullName: String {
if firstName.isEmpty {
return "<unkown> \(lastName)"
} else if lastName.isEmpty {
return "\(firstName) <unkown>"
} else {
return "\(firstName) \(lastName)"
}
}
}
When you write a computed property, you can write it with two different notations. One is the notation I have been using so far. The other is a slightly more specific version:
extension User {
var fullName: String {
get {
"\(firstName) \(lastName)"
}
}
}
In this notation, you explicitly define the getter for fullName
. It's not common to write your property like this if you don't also specify the setter for a property.
If your implementing conformance to a protocol, a computed property can be used to satisfy a protocol requirement that uses a get
property:
protocol NamedObject {
var firstName: String { get set }
var lastName: String { get set }
var fullName: String { get }
}
extension User: NamedObject {}
The User
object I've shown at the start of this post satisfies all requirements from the NamedObject
protocol. Because fullName
is a get
property, you can use a constant (let
) to satisfy this requirement, but a computed property would work as well and be more fitting in this case. In fact, because we can declare computed properties on extensions, it's possible to provide a default implementation for fullName
that's defined on NamedObject
:
extension NamedObject {
var fullName: String { "\(firstName) \(lastName)" }
}
Keep in mind that a computed property is evaluated every time you access it. This means that its value is always up to date, and computed based on the current state of the program. So in the case of fullName
, its value will always be based on the current values of firstName
and lastName
, no matter what.
While this is convenient in many cases, it can also get you in trouble if you're not careful. Let's go over some best practices to help you make sure that your computed property usage is appropriate and efficient.
Best practices when using computed properties
Knowing when you might want to use a computed property is key if you want to use them efficiently. In the previous section, I've shown you how computed properties work. A keen eye may have noticed that a computed property acts a lot like a function that takes no argument and returns a single value:
extension User {
var fullName: String {
"\(firstName) \(lastName)"
}
func fullName() -> String {
"\(firstName) \(lastName)"
}
}
I have found that a good rule of thumb for computed properties and this kind of function is to try and decide what the function does. If your function takes no arguments and only reads some values from its current environment without any side-effects, you're looking at a good candidate for a computed property. The lack of side-effects is absolutely crucial here. Accessing a property should not produce any side-effects. A side-effect, in this case, could be changing a property, kicking off a network call, reading from a database or really anything more than just passively reading the current state of the program.
Because a computed property is recomputed every time it's accessed it's important that you make sure your implementation of a computed property is efficient. If the code needed to compute a value is complex and potentially slow, consider moving this code into a function with a completion closure and performing the work asynchronously. Performing work asynchronously is something you cannot do in a computed property.
Another important consideration to take into account related to the property being recomputed for every access is to make sure you don't confuse a computed property with a lazy property that is initialized with a closure:
extension User {
var fullName: String {
"\(firstName) \(lastName)"
}
lazy var expensiveProperty: String = {
// expensive work
return resultOfExpensiveWork
}()
}
The expensiveProperty
in this example is initialized using the closure expression I assigned to it. It's also lazy which means it won't be evaluated until expensiveProperty
is accessed for the first time. This is very different from how a computed property works. The lazy property is only evaluated and initialized once. Subsequent access of the lazy property won't re-execute the expensive work. It's important to understand this difference because performing expensive or redundant work in a computed property can cause serious performance problems that can be very frustrating to track down.
In summary
In this post, I explained how computed properties work in Swift, and how you can use them effectively. I explained how you can declare computed properties in your codebase, and I've shown you that you can use computed properties to satisfy protocol requirements.
I moved on to explain some best practices surrounded computed properties and I've shown you that you can sometimes replace simple functions with computed properties to make your code easier to read. I also showed you the difference between a lazy property and a computed property, and I explained that it's important to understand the differences between the two to avoid potential performance issues.
If you have any questions or feedback about this post, don't hesitate to send me a Tweet.