8.
Value Types & Reference Types
Written by Alexis Gallagher
Swift supports two kinds of types: value types and reference types. Structs and enums are value types, while classes and functions are reference types. They behave differently. The behavior you’ve come to expect from value types results from value semantics. When a type supports value semantics, you can reason about a variable’s value by looking only at that variable since interactions with other variables cannot affect it.
Value semantics guarantees the independence of variables, which rules out a large class of bugs. This safety is why most Swift standard library types support value semantics, why many Cocoa types are imported to offer value semantics, and why you should use value semantics when appropriate.
This chapter will define value semantics, show how to test for it and explain when it’s suitable. You’ll learn to build types with value semantics by using value types, reference types, or some mix of the two. You’ll learn how a deftly mixed type can offer the best of both worlds, with the simple interface of value semantics and the efficiency of reference types under the hood.
Value Types vs. Reference Types
Value and reference types differ in their assignment behavior, which is just the name for what Swift does whenever you assign a value to a variable. Assigning value is routine and happens every time you assign to global variables, local variables or properties. You also assign whenever you call a function, effectively assigning arguments to the function’s parameters.
Reference Types
Reference types use assign-by-reference. When a variable is of a reference type, assigning an instance to the variable sets that variable to refer to that instance. If another variable was already referring to that instance, then both variables post-assignment refer to the same instance, like so:
Since both variables point to the same instance, you can use one variable to change that instance and see the change’s effect on the other.
Suppose you’re running a paint shop, selling paint to landscape artists, painters and builders. You’re building an inventory app to keep track of your paint.
Start with a simple color and paint abstraction:
struct Color: CustomStringConvertible {
var red, green, blue: Double
var description: String {
"r: \(red) g: \(green) b: \(blue)"
}
}
// Preset colors
extension Color {
static let black = Color(red: 0, green: 0, blue: 0)
static let white = Color(red: 1, green: 1, blue: 1)
static let blue = Color(red: 0, green: 0, blue: 1)
static let green = Color(red: 0, green: 1, blue: 0)
// more ...
}
// Paint bucket abstraction
class Bucket {
var color: Color
var isRefilled = false
init(color: Color) {
self.color = color
}
func refill() {
isRefilled = true
}
}
Landscape artists like painting the sky, so you have a bucket of blue paint in the shop with the label “azure” on the side. Housepainters also like that color but call it “wall blue.” So on the other side of that same bucket, you have another label that says “wall blue.”
The code in your inventory app reflects this:
let azurePaint = Bucket(color: .blue)
let wallBluePaint = azurePaint
wallBluePaint.isRefilled // => false, initially
azurePaint.refill()
wallBluePaint.isRefilled // => true, unsurprisingly!
When you call azurePaint.refill(), you also refill wallBluePaint because the two variables refer to the same instance.
The two variables now depend on each other. The value of any variable is simply the value of the instance it references, and these two variables refer to the same instance. Changing one might change the other, as the two variables are two names for the same bucket.
Value Types
Value types, however, use assign-by-copy. Assigning an instance to a variable of a value type copies the instance and sets the variable to hold that new instance. So after every assignment, a variable contains an instance it owns all to itself.
Here’s how this looks:
In the example above, Color is a value type, so assigning a value to wallBlue creates a copy of the instance held by azure.
With this system, each variable is independent, so you never need to worry that another variable might change it. For instance, suppose the painters’ tastes change, and they decide that walls look better in a darker shade of blue. If you call a method wallBlue.darken() to change the color of wallBlue, there is no effect on what is meant by azure.
extension Color {
mutating func darken() {
red *= 0.9; green *= 0.9; blue *= 0.9
}
}
var azure = Color.blue
var wallBlue = azure
azure // r: 0.0 g: 0.0 b: 1.0
wallBlue.darken()
azure // r: 0.0 g: 0.0 b: 1.0 (unaffected)
To continue the metaphor, instead of having different names for the same bucket of paint, where the bucket’s contents can change, these value-type variables are more like names printed on color sample swatches. Each name is independently associated with just one color.
Defining Value Semantics
What’s nice about primitive value types like Color or Int isn’t the assign-by-copy behavior itself but rather the guarantee this behavior creates.
The guarantee is that the only way to affect a variable’s value is through that variable itself. If a type promises that, then the type supports value semantics.
To test if a type supports value semantics, consider it in a snippet like the following:
var x = MysteryType()
var y = x
exposeValue(x) // => initial value derived from x
// {code here which uses only y}
exposeValue(x) // => final value derived from x
// Q: are the initial and final values different?
If the code which “uses only y” can affect the value of x, then MysteryType does not support value semantics.
One benefit of value semantics is that they aid local reasoning. To determine how a variable got its value, you only need to consider the history of interactions with that variable. Value semantics create a simple world, where variables have values unaffected by other variables.
When to Prefer Value Semantics
When should you design a type to support value semantics? This choice depends on what your type is supposed to model.
Value semantics are appropriate for representing inert, descriptive data — numbers, strings, and physical quantities like angle, length, or color; mathematical objects, like vectors and matrices; pure binary data; and lastly, collections of such values and large, rich structures made from such values, like media.
Reference semantics are suitable for representing distinct items in your program or the world. For example, specific objects or memory buffers that change over time and coordinate with other objects work well with reference semantics. Similarly, a particular person or physical object can be represented this way easily in the real world.
The underlying logic here is that the referenceable items are all objects, meaning they all have distinct identities. Two identical twins could be alike in all physical attributes, but they are still distinct people. Two buffers could hold equal byte patterns, but they’re still distinct buffers.
But the items on the value semantics list are all values. They lack identity, so talking about two things being equal but distinct is meaningless. If we agree x equals five, there is no further question about which five it equals. Five is five.
A typical pattern is to see a model type like Person defined as a reference type to reflect an object with identity. The type then uses other types with value semantics to store descriptive values like age, hairColor, etc..
When a program must represent many distinct items (like Persons), or when different parts of a program need to coordinate around the same item (like the UIScreen or the UIApplication instance of a UIKit app), reference types are the natural tool for representing those items.
UIKit, an object-oriented framework, uses reference types extensively so that distinct objects can communicate and interact. You have UIView instances for representing regions on the screen, UIScreen for the screen itself, NSNotificationCenter for objects providing framework services, and so on.
By contrast, SwiftUI is a declarative and more value-based framework. In this world, a View-conforming value type provides a lightweight, immutable description of a piece of the user interface computed from the current state. The framework efficiently re-renders the user interface whenever the state changes.
Implementing Value Semantics
Now assume you do want value semantics. If you’re defining a type, how do you enforce it? The approach depends on the details of the type. In this section, you’ll consider the various cases one by one.
Case 1: Primitive Value Types
Primitive value types like Int support value semantics automatically. This is because assign-by-copy ensures each variable holds its own instance, so no other variable can affect the instance and change its value.
A good intuition for a type like Int is that it’s a bit pattern with no external references or dependencies.
Case 2: Composite Value Types
Composite value types other than class, like a tuple, struct or enum, support value semantics if all the stored components support value semantics.
You can prove this rule by looking at how Swift does instance copying. When Swift copies the instance of a struct, it creates a copy instance as if directly assigning all the stored properties of the original instance into the copy instance’s properties. This assignment is direct in that it doesn’t invoke any property observers.
When you assign a struct value type, the assigned-to variable will hold a copy of the instance. If each property has value semantics itself, then the copy instance’s properties will be the only variables which can modify their instances. So from this, you can see the assigned-to variable is the only way to modify its instance or any other dependency. Therefore, this is the only way to alter its own value. Proof!
Tuples act as ad-hoc structs with no user-definable methods or protocol conformances, so the same proof logic applies.
The proof is analogous if the type is an enumeration. The instance copy gets the same enumeration case, and it’s as if that enumeration member’s associated values are directly assigned from the existing instance’s associated values.
Incidentally, since an Array<Element> provides the same semantics as a struct with a property of type Element, this case also tells you whether arrays support value semantics. They do, but only if their element type does.
Case 3: Reference Types
Reference types can also have value semantics.
To see how this is possible, recall that a type has value semantics if the only way to affect a variable’s value is through that variable. In general, you can change the value of a reference type in only two ways: First, by changing the value directly; second, by assigning it to a new variable and modifying that.
The first approach is allowed by value semantics. But the second way — modifying the instance through a newly assigned variable — must be prevented to preserve value semantics.
One solution is straightforward: Define the reference type to be immutable. In other words, build it so it’s impossible to change the instance’s value after initialization. To achieve this, you must ensure that all its stored properties are constant and only use types with value semantics.
Many of the basic UIKit utility types adopt this pattern. For instance, consider this code handling a UIImage:
var a = UIImage(named:"smile.jpg")
var b = a
computeValue(b) // => something
doSomething(a)
computeValue(b) // => same thing!
Because UIImage is immutable, there is no possible function doSomething(a) that will cause computeValue(b) to change the value it returns. It doesn’t matter if b is a copy of a.
The UIImage type has dozens of properties (scale, capInsets, renderingMode, etc.), but since they’re all read-only, you can’t modify an instance. Therefore, there’s no way for one variable to affect another. But if one of its properties were not constant, then setting that property would mutate the instance and spoil the invariant — such structural sharing of a common instance wouldn’t be safe.
UIImage, along with many of the Cocoa types, is defined as immutable because an immutable reference type has value semantics.
Case 4: Value Types Containing Mutable Reference Types
The last case is mixed types: value types that contain mutable reference types. This case is perhaps the most valuable, since it can provide the simple programming model of value semantics but with the efficiency benefits of reference types. However, it is subtle to implement correctly.
To see how it can fail, look again at the instance copying rule:
- When a mixed-type instance is copied, all of its properties are directly assigned.
- But since any reference-type property is assigned by reference to the copy, the instances of the copy property and the original property will refer to the same shared instance.
The instance and its copy are distinct, but their values depend on each other because of structural sharing, affecting both instances.
An example and a diagram will explain this best. Returning to your paint shop, imagine you want a type to define a plan for a painting project, a plan that specifies the bucket that provides the main color and also specifies the accent color:
struct PaintingPlan { // a value type, containing ...
// a value type
var accent = Color.white
// a mutable reference type
var bucket = Bucket(color: .blue)
}
You might want to define your plan for a piece of artwork by starting with a house painting plan and then modifying it. Since PaintingPlan is a struct — a value type — you might hope to do this by assigning a new variable and then modifying that variable.
Unfortunately, the assignment doesn’t create a genuinely independent copy since it’s a struct containing a reference type.
When you change the house plan color, you change the art plan’s color since they share the same bucket.
let artPlan = PaintingPlan()
let housePlan = artPlan
artPlan.bucket.color // => blue
// for house painting only, we fill the bucket with green paint
housePlan.bucket.color = Color.green
artPlan.bucket.color // => green. oops!
This surprising behavior is due to the implicit structural sharing of the paint bucket instance:
Because of this structural sharing, PaintingPlan is a value type but lacks value semantics. It does not have pure reference semantics, and it’s a mess.
You should beware of casual discussions of value semantics, which give the impression that all value types have value semantics or that having value semantics is synonymous with being a value type. As this example shows, this is simply an error. But it is a common one.
Copy-on-Write to the Rescue
What’s the fix? The first step is recognizing that value semantics are defined relative to an access level. Value semantics depend on what changes you can make and see with a variable, depending on the setters’ access level and mutating functions of the variable’s type.
So a type may provide value semantics to all client code — for example, which can access internal or public members — while not providing value semantics to code which can access its private members.
So the trick to preserving value semantics in a mixed type is to define the type such that its users can never see the effects of mutation on the contained reference-type property. This example makes the mutable reference type private and provides an interface that controls reads and writes:
struct PaintingPlan { // a value type, containing ...
// a value type
var accent = Color.white
// a private reference type for "deep storage"
private var bucket = Bucket()
// a pseudo-value type, using the deep storage
var bucketColor: Color {
get {
bucket.color
}
set {
bucket = Bucket(color: newValue)
}
}
}
To code which can access private members, this struct contains the mutable reference-type property bucket, spoiling value semantics. But to a client with internal access or higher, the type behaves like a struct with value semantics, with two properties, accentColor and bucketColor.
Reading bucketColor invokes the computed property getter, which reads from the private reference-type property bucket. This private property acts as the backing storage. Apple sometimes also calls this indirect storage or deep storage. Assigning to bucketColor invokes the computed property setter, designed to preserve the independence of PaintingPlan values. Whenever a user modifies bucketColor, the setter creates a new instance of indirect storage, a new Bucket, to back it.
The effect is that assigning a value of PaintingPlan does not immediately copy the backing storage at the moment of assignment, as with a simple value type. Instances will share their backing storage for a while. But every instance appears as if it always had its own backing store since it privately creates its own unique backing store as soon as one is needed.
This mechanism is called the copy-on-write (COW) pattern because the system only copies the backing store when writing to the variable.
But what’s the point of that? The point is performance. Suppose the backing store is enormous. When you only read from variables, the instances can all share the same backing store, using less storage and sparing the computational cost of copying it.
But once you use a variable to mutate an instance — to write to it — only then does the system copy the backing store to ensure the modification doesn’t affect other variables. This lazy approach minimizes immediate storage and compute costs, deferring them until needed.
Suppose the backing store is large enough to deserve this optimization. In that case, it’s worth applying a further optimization that performs in-place mutation of the backing store if it’s not shared elsewhere. This additional optimization is cheaper than creating a new store and discarding the old one.
For this to work, your value type needs a way to tell if it uniquely refers to a given backing store. The standard library function isKnownUniquelyReferenced provides just the thing for that:
struct PaintingPlan { // a value type, containing ...
// ... as above ...
// a computed property facade over deep storage
// with copy-on-write and in-place mutation when possible
var bucketColor: Color {
get {
bucket.color
}
set {
if isKnownUniquelyReferenced(&bucket) {
bucket.color = bucketColor
} else {
bucket = Bucket(color: newValue)
}
}
}
}
The Swift standard library uses this technique extensively.
Many Swift value types aren’t primitive value types but are mixed types that only seem like primitive value types because they provide value semantics, relying on efficient COW implementations. The Swift language itself also uses COW under the hood, sometimes deferring the copying of instances until the compiler can deduce that it’s needed because of a mutation.
Recipes for Value Semantics
To summarize, here is the recipe for determining if a type has value semantics or for defining your own such type:
For a reference type (a class):
- The type must be immutable, so the requirement is that all its properties are constant and must be of types that have value semantics.
For a value type (a struct or enum):
-
A primitive value type like
Intalways has value semantics. -
If you define a
structtype with properties, that type will have value semantics if all of its properties have value semantics. -
Similarly, if you define an
enumtype with associated values, that type will have value semantics if all its associated values have value semantics.
For COW value types —struct or enum:
-
Choose the “value-semantics access level”, that is, the access level which will expose an interface that preserves value semantics.
-
Note all mutable reference-type properties, as these are the ones that spoil automatic value semantics. Set their access level below the value-semantics level.
-
Define setters and mutating functions at and above the value-semantics access level so that they never actually modify a shared instance of those reference-type properties but instead assign a copy of the instance to the reference-type property.
Sidebar: Sendable
The benefits of value semantics are so substantial, and the recipe above is so formulaic, that you might wonder: couldn’t the compiler lend a hand?
For instance, wouldn’t it be nice if the compiler let you somehow mark a type as having value semantics? For instance, by letting you declare that a type is ValueSemantic? And if the compiler knew that primitive types like Int and String are all intrinsically ValueSemantic?
And that structs, enums, and tuples can only be ValueSemantic when all their members or associated values are ValueSemantic? And that class types can only be ValueSemantic when they contain only immutable stored properties that are also ValueSemantic? This reasoning, after all, is the essence of the recipe.
If the compiler knew all that, it could validate the types you declare as ValueSemantic. It could even generate those declarations, automatically detecting that certain types are ValueSemantic.
In fact, as of Swift 5.5, the compiler does this – but ValueSemantic is the new protocol Sendable, a marker protocol. Why finally introduce direct compiler support for a feature, value semantics, which has long been deeply embedded in the language and libraries implicitly? And why call it Sendable?
Recall that a key benefit of value semantics is that it makes types immune from side effects, aiding local reasoning. This property is invaluable in concurrent programming since it ensures you can pass a value from one concurrency domain to another completely, eliminating the risk that the value will be mutated from two concurrent domains. This guarantee is the motivation for Sendable.
Sendable is arriving in Swift as one of a set of carefully integrated features to support concurrent programming. It’s called “Sendable” to indicate that a value is safe to send from one domain to another. When the compiler sees code that tries to pass a non-Sendable value across domains, it raises an error at compile-time, preventing the sort of concurrency bug which is notoriously hard to understand at runtime. You’ll learn more about Swift’s concurrency features in Chapter 12, “Concurrency”.
So can you treat Sendable as a synonym for having value semantics? Not quite, because Sendable is designed primarily with concurrency in mind. For instance, there is no facility to specify an access level. However, Apple’s Swift documentation now specifies which types conform to Sendable in the documentation for Sendable. Also, for any type that conforms, Sendable is listed with the other protocols in the “Conforms To” section of that type and includes notes regarding any caveats. For example, the documentation notes that Array conforms to Sendable but only “when Element conforms to Sendable”. So it’s worth watching this protocol closely, to lean on it as an obvious, compiler-enforced way to keep track of value semantics, an essential aspect of a type which used to be visible only to those with a discerning eye.
Challenges
Before moving on, here are some challenges to test your knowledge of value types, reference types, and value semantics. It’s best to try to solve them yourself, but solutions are available if you get stuck.
These came with the download or are available at the printed book’s source code link listed in the introduction.
Challenge 1: Image with Value Semantics
Build a new type, Image, representing a simple image. It should also provide mutating functions which apply modifications to the image. Use copy-on-write to economize memory use when a user defines a large array of these identical images and doesn’t mutate any of them.
To get started, assume you’re using the following Pixels class for the raw storage:
private class Pixels {
let storageBuffer: UnsafeMutableBufferPointer<UInt8>
init(size: Int, value: UInt8) {
let p = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
storageBuffer = UnsafeMutableBufferPointer<UInt8>(start: p, count: size)
storageBuffer.initialize(from: repeatElement(value, count: size))
}
init(pixels: Pixels) {
let otherStorage = pixels.storageBuffer
let p = UnsafeMutablePointer<UInt8>.allocate(capacity: otherStorage.count)
storageBuffer = UnsafeMutableBufferPointer<UInt8>(start: p, count: otherStorage.count)
storageBuffer.initialize(from: otherStorage)
}
subscript(offset: Int) -> UInt8 {
get {
storageBuffer[offset]
}
set {
storageBuffer[offset] = newValue
}
}
deinit {
storageBuffer.baseAddress!.deallocate(capacity: self.storageBuffer.count)
}
}
Your image should be able to set and get individual pixel values and set all values at once. Typical usage:
var image1 = Image(width: 4, height: 4, value: 0)
// test setting and getting
image1[0,0] // -> 0
image1[0,0] = 100
image1[0,0] // -> 100
image1[1,1] // -> 0
// copy
var image2 = image1
image2[0,0] // -> 100
image1[0,0] = 2
image1[0,0] // -> 2
image2[0,0] // -> 100 because of copy-on-write
var image3 = image2
image3.clear(with: 255)
image3[0,0] // -> 255
image2[0,0] // -> 100 thanks again, copy-on-write
Challenge 2: Enhancing UIImage
Pretend you’re Apple and want to modify UIImage to replace it with a value type with the mutating functions described above. Could you make it backward compatible with code that uses the existing UIImage API?
Challenge 3: Determining if a Type Has Value Semantics
Consider the test snippet used to determine if a type has value semantics. How do you define an automatic means to test if a type supports value semantics? If I handed you a type, could you tell me if it offers value semantics? What if you could not see its implementation? Could the compiler be expected to know?
Key Points
-
Value types and reference types differ in their assignment behavior. Value types use assign-by-copy; reference types use assign-by-reference. This behavior describes whether a variable copies or refers to the instance assigned to it.
-
This assignment behavior affects not only variables but also function calls.
-
Value types help you implement types with value semantics. A type has value semantics if assigning to a variable seems to create a completely independent instance. When this is the case, the only way to affect a variable’s value is through the variable itself. You can then think about variables as if instances and references did not exist.
-
Primitive value types and immutable reference types have value semantics automatically. Value types containing reference types, such as mixed types, will only have value semantics if engineered that way. For instance, they might only share immutable properties or privately copy shared components on mutation.
-
Structural sharing is when distinct instances refer to a common backing instance that contributes to their value. This sharing economizes storage since multiple instances can depend on one large shared resource. But one instance can modify the shared backing instance. In that case, it can indirectly change the value of other instances so that the distinct instances are not fully independent, spoiling value semantics.
-
Copy-on-write is the optimization pattern where a type relies on structural sharing and preserves value semantics by copying its backing instance only when it is mutated. This sharing allows the efficiency of a reference type in the read-only case while deferring the cost of instance copying in the read-write case.
-
Reference types can also have value semantics if you define them as entirely immutable, meaning that they cannot be modified after initialization. The type’s stored properties must be read-only with value semantics to do this.
Where to Go From Here?
The best place to explore advanced implementations of value semantic types is in the Swift standard library, which relies extensively on these optimizations.
Apple and many practitioners in the wider community have written about value types and value-oriented programming more generally. Here are some relevant videos available online:
- WWDC 2016, session 207: What’s New in Foundation for Swift https://developer.apple.com/videos/play/wwdc2016/207/. Apple.
- WWDC 2015, session 414: Building Better Apps with Value Types https://developer.apple.com/videos/play/wwdc2015/414/. Apple.
- Controlling Complexity in Swift https://speakerdeck.com/andymatuschak/controlling-complexity-in-swift-or-making-friends-with-value. Andy Matuschak. (video unavailable)
- Value of Values https://www.infoq.com/presentations/Value-Values. Rich Hickey.
- Value Semantics versus Value Types https://youtu.be/DPadyMh5aXg?feature=shared. Alexis Gallagher (me!).
- Episode 71: “Polymorphic interfaces”, in Swift by Sundell https://www.swiftbysundell.com/podcast/71/. Dave Abrahams, a former member of the Swift core team.
These videos offer a perspective complementary to the one in this chapter. However, only the last two focus on the distinctions between value types defined by assignment behavior and value semantics defined by the independence of variable values. Dave’s discussion of value semantics, which starts around the 54-minute mark of the interview, is particularly helpful for seeing the historical roots in functional programming and C++ assignment behaviors and dispelling the widespread misunderstanding that “copy-on-write” is a kind of semantics rather than a performance optimization.