Everything is about mutation: Structs vs Classes

Ario Liyan
9 min readSep 12, 2022

--

This is deep dive into structs and classes. We are going to talk about: Reference types vs value types, Memory management: stack vs heap, Mutation, Copy-on-assignment, copy-on-write, and much more….

Sometimes immutables are more powerful!

Table of contents

  • Preface
  • Definitions
  • Similarities and differences
  • When we should use structs?
  • Why structs are used in SwiftUI as the foundation types?
  • Complementary notes

Preface

Both structs and classes are data types that we use, to store and create objects and instances from. They store grouped list of variables under one name in the memory, they also can have methods(functions) that we can call through their instances. There are lots of aspects of these two that are the same and on the other hand, there are a couple of differences that make them hugely different from each other, this makes the action of choosing them important. We first are going to talk about their definitions, similarities, and their differences.

Definitions

  • Class
  • Struct

Class

A class is a template for mimicking an object(the idea is based on real-world objects) in a real-world scenario. Same as real-world objects, classes have attributes and behaviors that define their concept. Technically talking(in object-oriented programming) a class is a data type that we can create as an extensible template for the concepts that we are interacting in our codes with. We have the ability to create objects(instances) from different classes.

A class can contain:

  • properties
  • methods
  • subscripts
  • initializers
  • protocol conformances
  • extensions

Code Example:

Struct

Structs were introduced a lot sooner than classes to the programming world, a struct is a composite data type(or some may say record), that we use to declare a complex data type. Structs grouped a list of variables under one name in a block of memory.

A struct just like a class can contain:

  • properties
  • methods
  • subscripts
  • initializers
  • protocol conformances
  • extensions

Code Example:

Similarities and differences

Reference types vs value types

Reference types
There are two kinds of types, reference types and value types. Variables of reference types store references to a memory block that contains the main data. If we have a couple of variables of the same data, they all are like pointers that point to the memory block. So if we change the data through one of them, whenever we use the other variables to get our hands on the data we can see that the data has been changed. So simply talking, with reference types, two variables can reference the same object; therefore, operations on one variable can affect the object referenced by the other variable.

Value types
Value types are somehow on the contrary side of the reference types, each variable creates a copy of the data and holds its copy for itself. So if one changes it doesn’t affect the others.

Conclusion
In swift and programming in general, structs are value types and classes are reference types. So an object created from a class is just a pointer to the memory block that contains our data but an instance of a struct holds the data directly. If we were to have a couple of variables pointing to an object of a class, whenever one of them changes the data, the data would change for all the others but it’s not true in case of structs.

Code example:

Memory management: stack vs heap

Memory management is an important criterion when it comes to making a decision to use classes or structs because in some cases it can have a huge impact on performance and user experience. There are two ways in which memory is managed in swift by using structs and classes, let’s dive into it:

Example of a real-world stack

Stack
First, we should wrap our minds around the stack concept. We can consider a stack an array or a list structure that we can access the data within it, through a special function(pop function). Unlike the normal array and lists, the data pushed in the stack can not be accessed arbitrarily, only the latest inserted data can be popped out of the stack(A stack works on LIFO (Last In First Out)). So for accessing the first inserted data we should pop all the others to get to the first pushed one.

In iOS, each thread has its own stack and the operating system allocates needed objects to the related stack of each thread(so the object on the stack memory is exclusive to that particular thread, note that this makes the pushed objects thread-safe.).

In stack memory, the operating system provides us with the amount of memory that the scopes of our program need to be run. Then the stack takes the same amount of memory and uses it to store objects until the scope(the part of the code that uses the stack) is de-scoped (i.e. the code has finished executing) the memory will be released.

Now in low-level programming, the cost of allocating and de-allocating the memory on a particular stack is the same as moving the stack pointer by the amount of required memory(it is executed by only one instruction).

So you may ask how is it related to structs and classes? The value types are all stored on the stack memory. Note that the value types should not have any reference types associated with them(i.e. they are either not contained by or contain a reference type) otherwise they wouldn’t be stored on the stack.

The amount of memory needed is normally calculated at compile time since they are not dynamic and do not need reference count semantics to decide how long they have to live(They live in a scope and when they are used the memory will dump them)

This can make value types so much faster than reference types which use heap memory.

Heap
Heap memory is a part of RAM that is allocated from the operating system to your app which is shared by all executing threads in the application. It is created on the Start-up process of every app.

Heap memory allocation is done for objects whose size can not be calculated at compile time, plus all reference types(because reference types life time is not based on their defined scope).

Heap allocation takes place on the heap data structure, which is generally more difficult to manage compared to a stack. Allocation and de-allocation of memory do not happen in a single instruction and therefore are a little more complex than the corresponding processes on a stack.

Note that heap memory is somehow a global host for objects and all threads can have access to it, so the objects stored on it are not thread-safe, this means that the onus is on you to manage safe-access from multi threads to the same objects.

As mentioned above reference types all use heap allocation.

Complementary notes on heap and stack allocation
There are cases where we face a Reference type containing a value type or a Value type containing a reference type, in the first scenario the reference type and the value type are going to be stored and managed on the heap and the reason for that is the value type associated with our reference type needs to be held in memory and it shouldn’t be de-allocated before the reference type is de-allocated. A very common but often overlooked example of this type is value types being used inside closures.

In the second case, the value type will not be allocated on the heap, but it will need to have reference-counting logic(we’ll talk about it later) associated with it since it now holds a reference type inside it. The reason for this is that if it doesn’t, then the reference type being held inside it can be de-allocated even though the value type itself is holding a reference to it.

More reading — ARC: Automatic Reference Counting

Mutation

Structs can only be changed if they are defined as variables otherwise they are immutable.

Initializers

Structs get a free initializer, unlike the class where you ought to implement an initializer for them. Of course, it is possible for you to implement your own initializer for structs too.

Inheritance

Inheritance is a concept in object-oriented(it means it only works on classes and not on structs) in which you have two types of classes, one superclasses or base classes as we say in swift lang and child classes when a child class inherits from a superclass it has access to all its superclass methods and variables and even can override them in most circumstances. But with the introduction of protocols(interface in C# and java) and extensions, the inheritance is somehow replaceable by using them with structs.

Deinitializers

A class allows executing code just before it gets destroyed by using a deinit method. When you define the same deinit method in a struct you’ll get the following error:

Deinitializers may only be declared within a class

Copy-on-assignment and copy-on-write

Copy-on-assignment behavior is like when you assign a struct to a new variable, a new copy of the struct is created.

Copy-on-write is a memory optimization that states that new value types are not created on assignment to a new member. They are instead created only when the value of the second member changes. This is done because copying value types that contain reference types have a huge overhead due to the reference-counting semantics that needs to be associated with them. It is an expensive operation.

This memory optimization is available only for certain special Swift structs (e.g. Array, Set, and Dictionary) and not for all value types containing reference types.

If you really want to, though, you can always create your own copy-on-write structs using this example.

Both these cases cause general overheads performance-wise to value types since they either need to be allocated differently or need to have some characteristics associated with them that are not their inherent behavior. That will need to be managed by Swift separately (which requires extra time).

When we should use structs?

The additional capabilities that classes support come at the cost of increased complexity. As a general guideline, prefer structures because they’re easier to reason about, and use classes when they’re appropriate or necessary. In practice, this means most of the custom data types you define will be structures and enumerations.
Documentation

Why structs are used in SwiftUI as the foundation types?

As you can think by now, structs are easier and faster so it’s a matter of performance.

In structs, we don’t have inheritance so unlike in UIKit where we used subclasses of UIView that sometimes inherit attributes and functions that they don’t use, it makes the code heavier.

And last but not least structs are immutable so our views are thread-safe and we have cleaner code in cases that we need to change the view we easily use state variables. For more read Link

Complementary notes

  • Comparing instance identity can be done by using ===
  • Use the final keyword when you want to use classes and you don’t need them as a superclass, in this way you tell the compiler that this class is not going to be inherited by any other classes.

--

--

Ario Liyan

As an iOS developer with a passion for programming concepts. I love sharing my latest discoveries with others and sparking conversations about technology.