Value and Reference Types
Swift types can, in general, be divided into two categories — value types and reference types — which determines how they will be handled between different functions and other code scopes. When using a value type, each instance is individually handled and mutated as a value, while reference type instances each act as a reference to an object. Let’s take a look at what that actually means, and what some of the practical implications are.
Let’s start with reference types, which in Swift essentially means types defined as a class. Say we’re working on a social networking app, and that we want to define a type to represent posts that users can publish. If we choose to make it a class it might look something like this:
class Post {
var title: String
var text: String
var numberOfLikes = 0
init(title: String, text: String) {
self.title = title
self.text = text
}
}
Next, let’s say that we want to write a function that can be called when a user presses some form of like button, which will increment the post’s numberOfLikes
and show a confirmation UI:
func like(_ post: Post) {
post.numberOfLikes += 1
showLikeConfirmation()
}
Putting both of the above pieces of code together, we can now create an instance of Post
, pass it to our like
function, and expect that printing the post’s numberOfLikes
will cause 1
to be displayed in the debug console:
let post = Post(title: "Hello, world!", text: "...")
like(post)
print(post.numberOfLikes) // 1
So far so good, and in general, the way class instances (or reference types) behave is often quite intuitive — especially for developers who have a background working with other object-oriented languages. If we pass an object to a function, then any mutations that occur within that function are also reflected outside of it — since we’re always referencing the original instance, even if we’re passing an object around to different parts of our code base.
Value types, on the other hand, behave quite differently. Let’s keep our Post
type exactly the same as before, and only change it from being a class to becoming a struct instead:
struct Post {
var title: String
var text: String
var numberOfLikes = 0
init(title: String, text: String) {
self.title = title
self.text = text
}
}
With the above change in place, the compiler will force us to modify our like
function a bit — since values passed into a function are constants by default, which means that they can’t be mutated in any way. So in order for us to be able to increment the passed post’s numberOfLikes
property, we need to create a mutable copy of it, like this:
func like(_ post: Post) {
// Simply re-assigning the post to a new, mutable, variable
// will actually create a new copy of it.
var post = post
post.numberOfLikes += 1
showLikeConfirmation()
}
However, the problem is that since we’re now copying the value, any changes we make to it within the scope of our like
function won’t be applied to the original Post
value we passed in — making our code from before now print 0
instead of 1
:
let post = Post(title: "Hello, world!", text: "...")
like(post)
print(post.numberOfLikes) // 0
One way to address the above problem would be to use the inout
keyword to turn our like
function’s Post
argument into a reference, even though it’s a value type. That way, we’re free to mutate the value inside of our function, and the changes will be applied to the original value that was passed in — just like when using a reference type:
func like(_ post: inout Post) {
post.numberOfLikes += 1
showLikeConfirmation()
}
The only difference is that, at the call site, we now need to pass our Post
value using the &
prefix — which indicates that we’re passing a value type as a reference, again resulting in 1
being printed as the number of likes:
var post = Post(title: "Hello, world!", text: "...")
like(&post)
print(post.numberOfLikes) // 1
While inout
does have its use cases, it’s arguably better to fully embrace the concept of value types, rather than treating them as references (if we need a reference, why not stick with using a class instead?). To do that, let’s instead have our like
function return a new, updated copy of the passed post — rather than attempting to mutate the original value:
func like(_ post: Post) -> Post {
var post = post
post.numberOfLikes += 1
showLikeConfirmation()
return post
}
With the above change in place, we can now simply assign the result of calling like
back to our original post
variable, to make sure that our outer scope reflects the changes made within our function:
var post = Post(title: "Hello, world!", text: "...")
post = like(post)
print(post.numberOfLikes) // 1
We could also take things a bit further, and add a mutating
API to Post
for incrementing the number of likes, enabling a post value to mutate itself:
extension Post {
mutating func like() {
numberOfLikes += 1
}
}
Using the above method, we could then also create another convenience API, which performs the copying and mutation required to like a post in one go:
extension Post {
func liked() -> Post {
var post = self
post.like()
return post
}
}
With the above in place, we can now go back to our like
function, and simplify it to only act as a wrapper for displaying the confirmation UI and for performing our model mutation, using our new convenience API:
func like(_ post: Post) -> Post {
showLikeConfirmation()
return post.liked()
}
When to use a value versus a reference type depends heavily on what kind of semantics that we want a type to have. Does it make the most sense for it to be treated as a simple value, that can only be locally mutated under specific circumstances, or does it make more sense for each instance to have an actual identity, and be passed as a reference?
Regardless of what we end up choosing, it’s often a better idea to lean into the semantics we picked — and adapt our code accordingly — rather than fighting the type system.
Thanks for reading! 🚀