Based on the memory handling methods, types in Swift are divided into two different categories: value types and reference types. There are some basic differences between value and reference types which causes them to behave differently in the same conditions. So developers should know their specifications to write logical codes both during working with Apple’s predefined types and during defining their own custom types.
To begin with, let’s have an overview of the characteristics of value and reference types.
Value types
- Copy by value: every time we assign one variable to another, or we pass a value type to a function, a copy is created instead.
- Mutability: If a value type is declared as a constant, none of its members can be mutated, as long as its members are value types themselves.
- Performance: value types are created in the stack memory, so they are a lot faster to create and manage in the memory.
- Inheritance: structs do not support inheritance.
Reference types
- Copy by reference: classes are copied by reference.
- Mutability: classes are not as strict about mutability as structs are.
- Inheritance: classes support inheritance.
The table below shows the reference and value types in Swift.
Reference Types | Value Types |
Class | Array |
Function | Dictionary |
Closure | Set |
Struct (all numbers like Int and Double, Character, and String are of the type of Struct) | |
Enum | |
Tuple |
Copy Method
The most basic distinguishing feature of value types is that copying value type instances or passing them into functions, create an independent instance of them while copying or passing reference types create a shared instance.
struct Person {
var name: String
}
var amir = Person(name: "Amir")
print("Amir name", amir.name)
var john = amir
john.name = "John"
print("John name:", john.name)
print("Amir name:", amir.name)
//Output
//Amir name Amir
//John name: John
//Amir name: Amir
It is clear that copying the Amir into John has created a new instance of Person. Now we are going to do the same for type Car which is same as Person but is of type Class instead of Struct.
class Car {
var name: String
init(name: String) {
self.name = name
}
}
let mercedes = Car(name: "Mercedes")
print("Mercedes name", mercedes.name)
var lexus = mercedes
lexus.name = "Lexus"
print("Lexus name:", lexus.name)
print("Mercedes name:", mercedes.name)
//Output
//Mercedes name Mercedes
//Lexus name: Lexus
//Mercedes name: Lexus
Obviously, by changing the lexus name, the mercedes name changed too that shows during the copying of the mercedes, only a reference of it has passed to the lexus, so both lexus and mercedes are referring to a shared instance.
So developers should be aware of this different behavior. One of the mistakes that programmers make normally at the start of programming with Swift is during working with arrays. They normally say I’m changing an array item but id doesn’t change. Look at the following code:
struct Person: CustomStringConvertible {
var description: String {
get {
return self.name
}
}
var name: String
}
var people = [Person(name: "John"),
Person(name: "Perter"),
Person(name: "Alice")]
print(people)
var alice = people[2]
alice.name = "Jafar"
print(people)
//Output
//[John, Perter, Alice]
//[John, Perter, Alice]
In the above code, I added the “CustomStringConvertible” to use the types easily with the print() method. As you see, even though I changed Alice but it didn’t change in the array. It is clear that copying the people[2] into the alice variable has created a new copy of the alice and this new one name was changed in the next line. So the array has not changed at all. Therefore to change the alice name we need to do it in the array like below.
print(people)
people[2].name = "Jafar"
print(people)
//Output
//[John, Perter, Alice]
//[John, Perter, Jafar]
Additionally, the following code works perfectly with an array of Cars. Now you know the reason. Because Car is a reference type.
class Car: CustomStringConvertible {
var description: String {
get {
return self.name
}
}
var name: String
init(name: String) {
self.name = name
}
}
var cars = [Car(name: "Mercedes"),
Car(name: "Lexus"),
Car(name: "BMW")]
print(cars)
var car = cars[2]
car.name = "Audi"
print(cars)
//Output
//[Mercedes, Lexus, BMW]
//[Mercedes, Lexus, Audi]
Mutability
After the difference in copy method, the most noticeable point in reference and value types is their mutability. Let’s test them with code.
let mike = Person(name: "Mike")
mike.name = "Mice"
In this case, we will encounter the compiler error: “Cannot assign to property: ‘mike’ is a ‘let’ constant”. Since we have defined mike as a constant, by the “let” keyword, the compiler does not allow us to mutate the instance. It sounds good because it makes our code more reliable predictable. What will happen if we do the same with a class like Car?
let mazda = Car(name: "Mazda")
mazda.name = "Nissan" //Compiles without error
mazda = Car(name: "Toyota") //Error: Cannot assign to value: 'mazda' is a 'let' contant
As you see, even though we have defined mazda as constant we can change the name of the mazda without error. But in the third line, we cannot assign a new car instance, toyota, to mazda. In fact, defining reference types as constant makes only the variable itself immutable, not its properties, which would not be very handy in many cases.
Defining Our Types
Must of the Swift predefined types, like enum and dictionary, have their own unique characteristics, so during development, deciding about using which of them is not difficult, but Class and Struct have similar functions with some important behavioral differences which makes selecting between them a little tricky.
For the struct, everything seems good, until a reference type as a property is added to it. Suppose each person can have a car.
struct Person: CustomStringConvertible {
var description: String {
get {
return "Name: \(self.name), Car: \(self.car.name)"
}
}
var name: String
var car: Car
}
let bob = Person(name: "Bob", car: Car(name: "Nissan"))
print(bob)
bob.car.name = "Volkswagen"
print(bob)
//Output
//Name: Bob, Car: Nissan
//Name: Bob, Car: Volkswagen
Though the bob was defined as constant, but the name of his car changed easily. Consequently, we lost the immutability feature of struct. Exactly the same way, if copy bob to another variable, mike, we will see that the bob’s car will be shared between bob and mike.
let bob = Person(name: "Bob", car: Car(name: "Nissan"))
print(bob)
let mike = bob
mike.car.name = "Kia"
print(bob)
//Output
//Name: Bob, Car: Nissan
//Name: Bob, Car: Kia
Hence, if we use reference type properties in a struct, we easily will lose the copy by value and immutability features. Therefore, as a rule of thumb:
Actually, one of the main use cases of structs could be data models. It means objects that only keep primitive data types and transfer them between the other objects. It is recommended to create data models by structs at first, then if we cannot accomplish our goals using structs, we can change them to class easily.
Conclusion
While structs seem very appealing at first glance, using them in the wrong way can lead us to severe bugs which finding them would be very difficult especially because we have some assumptions about structs that are not happening at the execution of our code. So to play safe, avoid using reference types inside structs. In addition, we should use classes whenever we need inheritance, definitely.
Comments
3 responses to “Swift Value and Reference Types, How to Use”
Great article! As a Java man, I’m in love with Swift. 🙂
But, as a lack of your description, you didn’t mention that Swift is pass by value or pass by type. Everything in Swift is passed by “copy” by default, so when you pass a value-type you get a copy of the value, and when you pass a reference type you get a copy of the reference.
So yes, Swift is pass by value like Java.
Hey Yousef, thanks for your comment. You’re right.
I forgot to say that if you pass a value type by inout keyword, it will pass that value type by ref. So you can edit that value inside the function body.