Struct vs Class in Swift

Swift Value types Struct Class Jan 7, 2021 · 9 min read

Background

In Swift, structs and classes have a lot of characteristics in common. Most other languages that also support structs are not as powerful as structs in Swift. But, keep in mind that there is a subtle but important difference between them that you should know as a developer because it is a very well-known question asked in almost all technical interviews. I always ask this question from my candidates. Unfortunately, the answers I got until this moment were either shallow or wrong or when I asked to provide examples, they failed. That's why I decided to write a blog post about it and close this topic once and for all.

You can achieve whatever you want just by using classes. Yes, you heard it right. We have been using only classes (even though when structs were more suitable) for many years both in statically and dynamically-typed languages. I believe structs were created to tremendously simplify software development and avoid common programming errors. You don't achieve a performance boost by using structs. Structs use more memory and degrade performance compared to classes especially when you start using a lot of copies of them in RAM and they hold a big and complex value. But, simplifying programming and codebase and avoiding common mistakes far more overweigh its small overhead over classes.

Similarities

Initializers, Properties, Methods, and Subscripts

This code helps to convert an instance of any struct or class conforming to the Encodable protocol to a dictionary.

import Foundation

extension Encodable {
    func asDictionary() throws -> [String: Any]? {
        let data = try JSONEncoder().encode(self)
        guard let dictionary = try JSONSerialization.jsonObject(
            with: data,
            options: .allowFragments
        ) as? [String: Any] else { return nil }
        return dictionary
    }
}

You can go and replace the struct keyword below with the class. Nothing changes. Everything works the same as before.

struct Person: Encodable {
    let name: String
    var age: UInt

    init(name: String, age: UInt) {
        self.name = name
        self.age = age
    }

    func walk() {
        print("Walking")
    }

    subscript(key: String) -> Any? {
        try? asDictionary()?[key]
    }
}

let person = Person(name: "Sukhrob", age: 32)
print(person.name) // Sukhrob
print(person.age) // 32
print(person["name"] as! String) // Sukhrob
print(person["age"] as! UInt) // 32
person.walk() // Walking

Protocols and Extensions

Both can conform to protocols and be extended using extensions. The example below shows how you can create a protocol with one method that has the default implementation using an extension. Protocols with extensions in Swift are a combination of interfaces and abstract classes in some other popular programming languages such as Java.

protocol CanCook {
    func cook()
}

extension CanCook {
    func cook() {
        print("Cooking")
    }
}
person.cook() // Cooking

You can override the default implementation both in structs and classes.

struct Person: CanCook, Encodable {
    // Overriding
    func cook() {
        print("Cooking Plov")
    }
}

person.cook() // Cooking Plov
class Person: CanCook, Encodable {
    // Overriding
    func cook() {
        print("Cooking Kazan Kebab")
    }
}

person.cook() // Cooking Kazan Kebab

Differences

Initializer

There is no need to explicitly define the initializer for the Person struct because structs in Swift support memberwise initializers which mean they have the default initializer with all the properties added to them whether those properties are required or optional. So, the code below works the same way.

struct Person: Encodable {
    let name: String
    var age: UInt

    func walk() {
        print("Walking")
    }

    subscript(key: String) -> Any? {
        try? asDictionary()?[key]
    }
}

Sometimes, you have to define it explicitly, for example, if you want to create a public or custom initializer because the memberwise initializer is internal by default.

struct Person: Encodable {
    let name: String
    var age: UInt

    public init(name: String, age: UInt) {
        self.name = name
        self.age = age
    }

    func walk() {
        print("Walking")
    }

    subscript(key: String) -> Any? {
        try? asDictionary()?[key]
    }
}

Deinitializer

Classes can have a deinitializer to control what to destroy and how to do it while structs can't.

struct Person: Encodable {
    // ...
    deinit() {} // Error: Deinitializers may only be declared within a class
}
class Person: Encodable {
    // ...
    deinit() {} // It Works
}

Value vs Reference types

Structs are value types and allocated in the Stack memory at a compile-time which is very fast. Structs are simple but yet powerful because they help you avoid common programming errors and are easy to reason about. For example, when you see an instance of a struct, you can be sure that no other part of your code is reading or changing its underlying data because you always work with a copy. Value types are thread-specific which means they are created and destroyed in the same thread which means structs are thread-safe and you don't have any threading issues with them in your code.

Classes are reference types and allocated in the Heap memory at a runtime that is a little bit slower. The system tracks reference types with Automatic Reference Counting (a.k.a Garbage Collection in some other popular languages such as Java). When you assign an instance of a class to another variable, both variables point to the same address in the Heap memory, and data is shared between them. You must be very careful with reference types in multi-threaded environments because sharing data between different threads might lead to race conditions or deadlocks.

There is no general rule whether a particular object must always be defined with a struct or class. It depends on the context you are working on. A struct is more suitable for some apps while the same object should better be a class type for other apps.

Inheritance

Classes can inherit from other classes and build a hierarchy of classes with a parent-child relationship while structs can't. But, structs can conform to protocols like classes and inherit the default implementation as you saw with the Person struct and CanCook protocol. In other words, structs can conform to protocols and inherit their default implementation but can't inherit from other classes or structs.

class Person: CanCook, Encodable {
    // ...
}

class Driver: Person { // It works
    func drive() {
        print("Driving")
    }
}
struct Person: CanCook, Encodable, Equatable {
    // ...
}

struct Driver: Person { // Error: Inheritance from non-protocol type 'Person'
    func drive() {
        print("Driving")
    }
}

Identity and Equality

An instance of a struct has no identity. Two instances of the same struct are equal if all of their corresponding properties are equal.

struct Person: CanCook, Encodable, Equatable {
    // ...
}

var person1 = Person(name: "Sukhrob", age: 32)
var person2 = person1
print(person2 == person1) // true
person2.age = 30
print(person2 == person1) // false

An instance of a class has an identity. Two instances of the same class are equal if both point to the same address in the Heap memory regardless of their corresponding properties being equal. For example, if two people have the same name and age, it doesn't mean they are the same person.

class Person: CanCook, Encodable {
    // ...
}

var person1 = Person(name: "Sukhrob", age: 32)
var person2 = person1
var person3 = Person(name: "Sukhrob", age: 32)
var person4 = Person(name: "Nuriddin", age: 30)
print(person2 === person1) // true
person2.age = 30
print(person2 === person1) // true
print(person3 === person1) // false
print(person4 === person1) // false

Mutability

If you assign a new instance of a struct to a constant, all of its properties are immutable (a.k.a read-only) whether they are constants or variables because the whole instance of a struct is immutable. If you assign it to a variable, only constants are immutable while variables can be changed.

let person = Person(name: "Sukhrob", age: 32) // struct Person
person.name = "Nuriddin" // Error: Cannot assign to property: 'person' is a 'let' constant
person.age = 30 // Error: Cannot assign to property: 'person' is a 'let' constant
var person = Person(name: "Sukhrob", age: 32) // struct Person
person.name = "Nuriddin" // Error: Cannot assign to property: 'name' is a 'let' constant
person.age = 30 // It works

As for a class, it doesn't matter whether you assign it to a constant or variable. The result is the same.

let person = Person(name: "Sukhrob", age: 32) // class Person
person.name = "Nuriddin" // Error: Cannot assign to property: 'name' is a 'let' constant
person.age = 30 // It works
var person = Person(name: "Sukhrob", age: 32) // class Person
person.name = "Nuriddin" // Error: Cannot assign to property: 'name' is a 'let' constant
person.age = 30 // It works

Performance

Structs tend to use more memory compared to classes because more and more copies are created when you assign a new value, change their properties, or pass them as parameters in functions. They might also degrade the performance especially when the underlying value is quite big and copying it is very expensive. To minimize this issue, the Swift Standard Library implements the Copy-On-Write technique for some of its value types such as Array and Dictionary. What it means is that copying is delayed until the instance is mutated and there is more than one reference to it. But, when you come across the same problem with your custom structs that store a big and complex value, you have to implement the Copy-On-Write technique on your own. Here is an example of how to do it.

struct Person: CanCook, Encodable {
    var name: String { _person.name }
    var age: UInt {
        get { _person.age }
        set {
            if isKnownUniquelyReferenced(&_person) {
                _person.age = newValue
                print("The same instance is changed")
            } else {
                _person = _Person(name: _person.name, age: _person.age)
                print("A new copy is created")
            }
        }
    }
    private var _person: _Person

    init(name: String, age: UInt) {
        _person = _Person(name: name, age: age)
    }

    func walk() {
        _person.walk()
    }

    subscript(key: String) -> Any? {
        _person[key]
    }

    func cook() {
        _person.cook()
    }
}

extension Person {
    private class _Person: CanCook, Encodable {
        let name: String
        var age: UInt

        init(name: String, age: UInt) {
            self.name = name
            self.age = age
        }

        func walk() {
            print("Walking")
        }

        subscript(key: String) -> Any? {
            try? asDictionary()?[key]
        }

        func cook() {
            print("Cooking Plov")
        }
    }
}
var person = Person(name: "Sukhrob", age: 32)
person.age = 30 // The same instance is changed
var person1 = Person(name: "Sukhrob", age: 32)
var person2 = person1
person2.age = 30 // A new copy is created

Conclusion

Since structs and classes have a lot of similarities in Swift, you might wonder when to use which. The rule of thumb is to ask yourself "Does this object have an identity?". In other words, are two instances of the same object equal if all of their corresponding properties are equal? If yes, then use a struct. Otherwise, use a class. But, try to use a struct by default if you don't know the answer yet. Also, you may have to use the Copy-On-Write technique for a struct with a class which holds the underlying value to minimize copying and boost the performance of your apps. There are very rare situations where you have to do this. But, it is good to know about this technique and know how to apply it when needed.