on
Swift - Reference vs Value types
The Swift language provide different ways of storing data using structs, enums or classes. Classes are reference types whereas structs and enums are value types.
Reference types
Classes and functions (including closures) are reference types in Swift. Reference types are designed to share mutable state across threads.
Classes represent objects with a clear lifecycle (e.g initialized, changed and destroyed). Class instances are accessed directly through their references and can have multiple owners. Closures can capture variables outside of their scope and keep a strong reference to them (as long as these variables are reference types too).
Reference types are compared using their memory addresses with the ===
operator.
class Gibson {
var model: String
var year: Int
init(model: String, year: Int) {
self.model = model
self.year = year
}
}
var hummingbird = Gibson(model: "Hummingbird", year: 1990)
var songwriter = hummingbird {
didSet {
print("The new Gibson model is \(songwriter.model)")
}
}
hummingbird.model
songwriter.model
hummingbird === songwriter
// prints "Hummingbird"
// prints "Hummingbird"
// prints "true"
The ===
operator returns a Boolean value indicating whether two references point to the same object instance.
After assigning the hummingbird class instance to the songwriter variable, both instances will point to the same object in memory.
The didSet
block is never triggered because the object reference doesn’t change, only the underlying model value does.
This is key to understanding how reference types work compared to value types.
Changing the model of one of the class instances will have a direct impact on the other one.
songwriter.model = "Songwriter"
// prints “Songwriter”
// prints “Songwriter”
Reference types are stored on the heap and must be deallocated from memory once they have reached their final destroyed state.
While Automatic Reference Counting (ARC) efficiently tracks and manages the app’s memory usage — doing a lot of the work for us — , we still need to pay attention to our objects behavior.
Whenever two objects keep a strong reference to each other, a retain cycle is created (their reference count won’t decrease) which will result in memory leaks — an object no longer used in our program but still alive in memory — .
Using weak
or unowned
on these references will address this issue.
Value types behave differently.
Value types
Value types are designed to store immutable values that won’t be shared across threads. Structs are value types which have copy semantics that is they can only have a single owner. Value types, as opposed to reference types, enable us to store data — without a lifecycle — on the stack.
The Swift standard library uses structs to model most of its types.
public struct URL : ReferenceConvertible, Equatable {
public typealias ReferenceType = NSURL
public typealias BookmarkResolutionOptions =
NSURL.BookmarkResolutionOptions
public typealias BookmarkCreationOptions =
NSURL.BookmarkCreationOptions
public init?(string: String)
public init?(string: String, relativeTo url: URL?)
...
}
Interestingly enough, the URL
struct takes a typealias ReferenceType
referring to its Objective-C counterpart NSURL
which is a reference type (the NSURL
class).
Many of the classes in Foundation
actually have structs counterparts specifically built for Swift which is a clear indicator on how important structs are in the language.
struct Guitar {
var model: String
let year: Int
}
var gibson = Guitar(model: "Songwriter", year: 1990)
var gibsonCopy = gibson
gibsonCopy.model = "Hummingbird"
// prints "Songwriter"
// prints "Hummingbird"
When assigning the gibson
instance to the gibsonCopy
variable, a copy of the struct is actually created.
Changing the model
property will then only affect the gibsonCopy
instance.
var gibson = Guitar(model: "Songwriter", year: 1990) {
didSet {
print("The new Gibson model is \(gibson.model)")
}
}
gibson.model = "Hummingbird"
// prints "The new Gibson model is Hummingbird"
Although we declared the model property as a variable and then mutated it, a whole new struct will be created and assigned to the gibson
instance using the new value, hence triggering the didSet
block.
This is key to understanding value types.
Declaring such the following method inside the Gibson
struct will raise an error.
struct Guitar {
var model: String
let year: Int
func assign(model: String) {
self.model = model
// Cannot assign to property: ‘self’ is immutable
}
}
The compiler enforces immutability by preventing us from mutating any part of self inside the method.
To change this behavior, the mutating
keyword must be used.
mutating func assign(model: String) {
self.model = model
}
The mutating
keyword is particularly useful when it comes to mutating Self
instead of returning a copy.
The append(_ newElement:)
method from Array
mutates Self
.
@inlinable public mutating func append(_ newElement: Element)
Sometimes it might be necessary to use both variants.
@inlinable public func sorted() -> [Element]
@inlinable public mutating func sort()
The sorted()
method will return a copy whereas the sort()
method will sort the elements in place.
Mutating methods act just like regular methods except Self
is marked as inout
enabling us to mutate it.
The inout
and mutating keywords indeed behave similarly.
The inout
parameter enables to mutate an argument within a function body overwriting the original value.
Erasing the mutating assign
method and replacing it with a global function with an inout
parameter will result in the following implementation.
func assign(guitar: inout Guitar) {
guitar.model = "Hummingbird"
}
var gibson = Guitar(model: "Songwriter", year: 1990)
assign(guitar: &gibson)
// prints "The new Gibson model is Hummingbird"
The value types behavior may sound expensive as values must be copied each time however the compiler is smart enough to optimize these copy operations.
The technique is referred to as copy-on-write
and it comes for free with Array
, Dictionnary
and Set
.
var x = [2, 4, 6, 8]
var y = x
A copy of x
gets made and assigned to y
thus x
and y are now two independent structs.
x
and y
reference to the same memory buffer (where the elements are actually stored).
The buffer has two owners.
Both arrays share the same storage and will continue to do so until y
gets mutated.
y.append(10)
y
has now changed triggering a copy of the original buffer and mutating it, leaving the original buffer in place.
If x
was the unique owner of the buffer, the buffer would be mutated in place.
While the copy-on-write
behavior is automatically implemented when dealing with arrays, dictionaries or sets, it doesn’t come for free when dealing with our own structs.
class Gibson {
var model: String = ""
var year: Int = 1990
}
struct Guitar {
var isAvailable = true
var brand = Gibson()
}
var hummingbird = Guitar()
hummingbird.brand.model = "Hummingbird"
var songwriter = Guitar()
songwriter.brand = hummingbird.brand
Now, both brand instances points to the same object.
hummingbird.brand.model = "Songwriter"
Unfortunately, because the brand
property use reference semantics, changing the model name will affect both hummingbird and songwriter instances.
hummingbird.brand.model
songwriter.brand.model
// prints "Songwriter"
// prints "Songwriter"
Using the copy-on-write
technique comes in handy when our struct contains a mutable reference that should behave like a value semantics type.
class Gibson: NSCopying {
var model: String = “”
var year: Int = 1990
func copy(with zone: NSZone? = nil) -> Any {
let copy = Gibson()
copy.model = model
copy.year = year
return copy
}
}
struct Guitar {
var isAvailable = true
private var _brand = Gibson()
var brand: Gibson {
mutating get {
if !isKnownUniquelyReferenced(&_brand) {
_brand = _brand.copy() as! Gibson
}
return _brand
}
set {
_brand = newValue
}
}
}
The isKnownUniquelyReferenced()
function returns a Boolean value indicating whether the given object is known to have a single strong reference.
It enables us to check whether the given reference has only one owner.
In our struct, we make our _brand
property private and use a getter to return either a copied instance in case of multiple owners or the current instance in case of a single owner.
We need to use the mutating
keyword on the getter
otherwise the compiler will complain.
hummingbird.brand.model
songwriter.brand.model
// prints "Songwriter"
// prints "Hummingbird"
Beware though that isKnownUniquelyReferenced()
is designed only for Swift classes.
Conclusion
Structs are used to store data that mustn’t be shared across threads while classes are convenient when dealing with mutable data.
Because objects are dynamically allocated on the heap with a clear lifecycle, they must be carefully handled using weak or unowned to avoid retain cycles and optimize memory usage.
Using copy-on-write
technique enable us to retain value semantics on objects within a struct.