Struct and class are building blocks of our program’s code. Through it, we have defined the properties and methods to add functionality in it by using the constants, variables, closures, protocols, and functions.
Unlike other programming languages, Swift doesn’t require us to create separate interface and implementation files for custom structures and classes. In Swift, we define in a single file and the external interface that it is automatically made available for other code to use.
According to the very popular WWDC 2015 talk Protocol Oriented, Swift provides a number of features that make structs better than classes in many circumstances.
Classes
In OOP, a class is a blueprint from which individual instances are created. It is the reference types and every change in a reference type will modify the value allocated in that place of memory or reference.
It is a blueprint for creating objects (a particular data structure), providing initial values for state (member variables or attributes), and implementations of behavior (member functions or methods).
We define a class using the class
keyword.
Structures
A struct is similar to a class in terms of definition and instance creation. It is the value types and that means that every change on them will just modify that value.
There is a very powerful feature of Swift, and they can help to make your code more reusable, more flexible, and less tightly coupled. And last but not least, they sharpen your skills as an app developer!
We define a struct using the struct
keyword.
Comparing Struct and Class
Common factors between struct and class:
- Define properties to store values, and they can define functions
- Also, provide access to values with subscript syntax
- Define initializers to set up their initial state, with
init()
- Extended with
extension
(this is important!) - Conform to protocols, for instance, to support Protocol Oriented Programming
Classes have benefits over structures :
- Inheritance enables one class to inherit the characteristics of another.
- Typecasting enables us to check and interpret the type of a class instance at runtime.
- Deinitializers enable an instance of a class to free up any resources it has assigned.
- Reference counting allows more than one reference to a class instance.
Structures have benefits over classes :
- Structs are much safer and bug-free, especially in a multithreaded environment. Swift value types are kept in the stack. In a process, each thread has its own stack space, so no other thread will be able to access your value type directly. Hence no race conditions, locks, deadlocks or any related thread synchronization complexity.
- Even though struct and enum don’t support inheritance, they are great for protocol-oriented programming. A subclass inherits all the required and unwanted functionalities from the superclass and is a bad programming practice. Better to use a struct with protocol-oriented programming concept which fixes the above-said issue.
- Class is a reference type and is stored in the heap part of memory which makes a class comparatively slower than a struct in terms of performance. Unlike a class, a struct is created on the stack. So, it is faster to instantiate (and destroy) a struct than a class.
- Value types do not need dynamic memory allocation or reference counting, both of which are expensive operations. It dispatched statically. These create a huge advantage in favor of value types in terms of performance.
- Structs are preferable if they are relatively small and copiable because copying is way safer than having multiple references to the same instance as happens with classes. This is especially important when passing around a variable to many classes and/or in a multithreaded environment. If we can always send a copy of your variable to other places, we never have to worry about that other place changing the value of your variable underneath we.
Value Types and Reference Types
Value Type:
When we copy a value type (i.e., when it’s assigned, initialized or passed into a function), each instance keeps a unique copy of the data. If we change one instance, the other doesn’t change too.
Let’s take a look at an example:
Ranjit has some notes, and he gives it to Vicky. Vicky writes it down and now his has own copy. When he accidentally changes it, only his copy changes and not the original notes Ranjit has. Both Ranjit and Vicky have their unique copy of the notes.
Swift’s struct
can change their mutability status:
let
+ struct
= immutable
= constant of value
It can not be reassigned or changed
var
+ struct
= mutable
It can be reassigned or changed
1 2 3 4 5 6 7 8 9 10 11 |
//let + struct let letStructA = StructA() letStructA.a = 5 //Compile ERROR: Cannot assign to property: 'structALet' is a 'let' constant letStructA = StructA() // Compile ERROR: Cannot assign to value: 'structALet' is a 'let' constant //var + struct var varStructA = StructA() varStructA.a = 5 varStructA = StructA() // |
Reference Type:
When we copy a reference type, each instance shares the data. The reference itself is copied, but not the data it references. When we change one, the other changes too.
Let’s take a look at an example:
Ranjit has some notes. He gives it to Vicky. Vicky doesn’t write down the notes for himself but instead remembers that Ranjit has it. When he needs those notes, he asks Ranjit. When Vicky accidentally changes in their notes, Ranjit’s notes change too.
Swift’s classes
are mutable a-priory:
let
+ class
= constant of address
It can not reassign and can change
var
+ class
It can reassign or change
1 2 3 4 5 6 7 8 9 10 11 |
//let + class let letClassA = ClassA() letClassA.a = 5 letClassA = ClassA() // Compile ERROR: Cannot assign to value: 'classALet' is a 'let' constant //var + class var varClassA = ClassA() varClassA.a = 5 varClassA = ClassA() |
Why Choose Struct Over Class?
Consider the following example, which demonstrates 2 strategies of wrapping Int
datatype using struct
and class
.
First, we can use one repeated value to better reflect the real world, where we get.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class IntClass { let value: Int init(_ val: Int) { self.value = val } } struct IntStruct { let value: Int init(_ val: Int) { self.value = val } } func + (x: IntClass, y: IntClass) -> IntClass { return IntClass(x.value + y.value) } func + (x: IntStruct, y: IntStruct) -> IntStruct { return IntStruct(x.value + y.value) } |
So Performance measured by using :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
static func runCheck() { print("Running tests") measure("class (1 field)") { var x = IntClass(0) for _ in 1...10000000 { x = x + IntClass(1) } } measure("struct (1 field)") { var x = IntStruct(0) for _ in 1...10000000 { x = x + IntStruct(1) } } } static private func measure(_ name: String, block: @escaping () -> ()) { print() print("\(name)") let t0 = CACurrentMediaTime() block() let dt = CACurrentMediaTime() - t0 print("\(dt)") } |
We have to initialize it.
1 2 3 4 5 |
override func viewDidLoad() { super.viewDidLoad() ViewController.runCheck() } |
As of Swift 5.0, Xcode 11.3, running Release build on iPhone 11 pro-Max, iOS 13.2.0, Swift Compiler setting is -O -whole-module-optimization
:
class
version took 19.857463046999328 secondsstruct
version took 12.641430059000413 seconds
Now we reflect the example in 10 fields for struct and class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class IntClass { let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int init(_ val: Int) { self.value1 = val self.value2 = val self.value3 = val self.value4 = val self.value5 = val self.value6 = val self.value7 = val self.value8 = val self.value9 = val self.value10 = val } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct IntStruct { let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int init(_ val: Int) { self.value1 = val self.value2 = val self.value3 = val self.value4 = val self.value5 = val self.value6 = val self.value7 = val self.value8 = val self.value9 = val self.value10 = val } } |
1 2 3 4 5 6 7 |
func + (x: Int10Struct, y: Int10Struct) -> Int10Struct { return Int10Struct(x.value1 + y.value1) } func + (x: Int10Class, y: Int10Class) -> Int10Class { return Int10Class(x.value1 + y.value1) } |
Now Performance measured for 10-10 field in class and struct:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static func runTests() { print("Running tests") measure("class (10 fields)") { var x = Int10Class(0) for _ in 1...10000000 { x = x + Int10Class(1) } } measure("struct (10 fields)") { var x = Int10Struct(0) for _ in 1...10000000 { x = x + Int10Struct(1) } } } |
We have to initialize it for the performance.
1 2 3 4 5 |
override func viewDidLoad() { super.viewDidLoad() ViewController.runTests() } |
As of Swift 5.0, Xcode 11.3, running Release build on iPhone 11 pro-Max, iOS 13.2.0, Swift Compiler setting is -O -whole-module-optimization
:
class
version took 26.345181417000276 secondsstruct
version took 17.24558438400345 seconds (which is faster than the 1 field of class).
Note: The difference is a lot less dramatic without whole module optimization. I’d be glad if someone can point out what the flag does.
We should default to using classes and use structures only in specific circumstances. Ultimately, we need to understand the real-world implication of value types vs. reference types and then we can make an informed decision about when to use structs or classes.
Conclusion
So, if you have any comments, questions, or recommendations, feel free to post them in the comment section below!