Understanding Swift Value Semantics

Even if you’re just getting started with programming, chances are big that you’ve stumbled across Value Types. You may not even realize you’re working with them, but they come with some very interesting and good-to-know attributes that we will look into in this article.

Conceptual sketch of a stack storing two Value Types.

What Are Value Types?

You may know that a class is a Reference Type and that you are able to share the reference to an object between different variables. You may also know that this feature enables you to change the state of said object through not just one, but infinitely many variables like so:

class MyReferenceType {
var a: Int
init(a: Int) {
self.a = a
var R1 = MyReferenceType(a: 1)
var R2 = R1
R2.a = 42
print(R1.a, R2.a)
// Prints "42 42"

The fact that many different variables are able to mutate the same object is sometimes referred to as an alias problem, and it can produce some seriously hard-to-track-down bugs if you’re not careful.

A Value Type, on the other hand, behaves more like we’d expect the standard primitive types to behave. This means that all the things you would expect an int, double, or float to do, a Value Type will do too. For example, if you perform the operations below, you would expect the variable b to have its own copy of the number 42 that it can alter, and that the variable a remains untouched when b is mutated.

var a: Int = 42
var b: Int = a
b = 0
print(a, b)// Prints "42 0"

These semantics extend into other Value Types as well. In Swift, a struct is a Value Type, and so are enum and tuple values. This means that a new object is allocated whenever a variable is assigned the value of another variable. Take a look at the code below and see that mutating V2 does not alter the value of V1:

struct MyValueType {
var a: Int
var V1 = MyValueType(a: 1)
var V2 = V1
V2.a = 42
// Prints "MyValueType(a: 1)"
// Prints "MyValueType(a: 42)"

Let’s dive deeper…

As you saw in the previous section, Reference Types and Value Types differ in some fundamental ways that are pretty easy to make out. But there are also times when the line between them gets blurry, like with the Array class (which is a struct). We’re going to use a helper method that enables us to compare memory addresses to get a closer look at what happens.

func address(of pointer: UnsafeRawPointer) -> String {
let length = 2 + 2 * MemoryLayout<UnsafeRawPointer>.size
let address = Int(bitPattern: pointer)
return String(format: "%0\(length)p", address)
var V1 = [Int]()
var V2 = V1
print(address(of: &V1) == address(of: &V2))
// Prints "true"
V1 = [Int]()
V2 = [Int]()
print(address(of: &V1) == address(of: &V2))
// Prints "true"

What happened here? Even though our array is a Value Type, it is not copied to a new memory location when it’s assigned to our V2 variable. Not even when we create and assign new array instances to each of the variables do they land on different memory addresses. So what’s going on?

The Swift compiler is a smart thing, and it will do a whole bunch of optimizations to make sure that your program consumes the least amount of resources possible. In the example above, the compiler notices that the two variables are pointing towards two equal arrays. Since none of them are mutated during execution, we save ourselves some memory and execution time by allocating one instance and then pointing both our variables towards that same instance. The sharing of resources that aren’t changed in any way is an optimization behaviour called “Copy-On-Write”.

The Copy-On-Write Mechanism

A bunch of Swift Value Types implement the “Copy-On-Write” behaviour (COW), which means that several variables can point towards the same memory location as long as they don’t make any adjustments to the contents in that location. If one variable wants to mutate the data, it will need to allocate new memory, copy the contents over there and then make adjustments to the structure over at the new memory address. In practice, this means that a variable holding a COW value will not have “exclusive rights” to that allocation in the same way as non-COW variables. Using the address method in our example with the MyValueType struct would show that V2 had allocated its own memory and just copied the values of V1 to that location, which means that the first MyValueType object will, in a sense, be exclusive to our V1 variable.

To prove that the variable that is assigned the initial value not necessarily is the one that gets to keep it, I’ll post a screenshot from an XCode Playground. Take a look at the printed memory addresses and you’ll see that the first variable actually had to surrender the address and object it was assigned at initialization, simply because it was the first one to want to mutate the underlying value after it was shared and therefore had to allocate new memory for the mutation.

That’s it for this week! If you want to learn more about Value Types and what advantages it can bring to your projects, I recommend watching this Apple presentation from WWDC 2015.

Feel free to comment if you have questions, and follow to get notifications about future articles.

Data Scientist | Software Engineer | Author

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store