Classes and Structures in Swift iOS 18.08.2018

In Swift, classes and structures are very similar. Apple describes classes and structures as follows:

Classes and structures are general-purpose, flexible constructs that become the building blocks of your program's code. You define properties and methods to add functionality to your classes and structures by using the already familiar syntax of constants, variables, and functions.

Let's begin by taking a quick look at some of the similarities between classes and structures.

Similarities between classes and structures

The following is a list of some of the features that classes and structures share:

  • Properties: These are used to store information in our classes and structures.
  • Methods: These provide functionality for our classes and structures.
  • Initializers: These are used when initializing instances of our classes and structures.
  • Subscripts: These provide access to values using the subscript syntax.
  • Extensions: These help extend both classes and structures.

Now let's take a quick look at some of the differences between classes and structures.

Differences between classes and structures

While classes and structures are very similar, there are also several very important differences. The following is a list of some of the differences between classes and structures in Swift:

  • Type: A structure is a value type, while a class is a reference type. Value types do not incur the additional overhead for reference counting that reference types incur. Value types are also stored on the stack, which provides better performance as compared to reference types, which are stored on the heap. It is also worth noting that copying values is relatively cheap in Swift.
  • Inheritance: A structure cannot inherit from other types, while a class can
  • Deinitializers: Structures cannot have custom deinitializers, while a class can

Keep in mind that, as our value types get large, the performance cost of copying can negate the other performance gains of value types. In the Swift standard library, Apple has implemented copy-on-write behavior to reduce the overhead of copying large value types.

With copy-on-write behavior, we do not create a new copy of our value type when we assign it to a new variable. The copy is postponed until one of the instances changes the value. This means that, if we have an array of one million numbers, when we pass this array to another array we will not make a copy of the one million numbers until one of the arrays changes. This can greatly reduce the overhead incurred from copying instances of our value types.

Value types are also a lot safer than reference types, because we do not have multiple references pointing to the same instance, as we do with reference types. This really becomes apparent when we are dealing with a multithreaded environment. Value types are also safer because we do not have memory leaks caused by common programming errors, such as the strong reference cycles.

Value versus reference types

To fully understand when to use classes and structures and how to properly use them, it is important to understand the difference between value and reference types.

Structures are value types. When we pass instances of a structure within our application, we pass a copy of the structure and not the original structure. Classes are reference types, therefore when we pass an instance of a class, within our application, a reference to the original instance is passed. It is very important to understand this difference.

When we pass structures within our application, we are passing copies of the structures and not the original structures. Since the function gets its own copy of the structure, it can change it as needed without affecting the original instance of the structure.

When we pass an instance of a class within our application, we are passing a reference to the original instance of the class. Since we're passing the instance of the class to the function, the function is getting a reference to the original instance; therefore, any changes made within the function will remain once the function exits.

Creating a class or structure

We use the same syntax to define classes and structures. The only difference is that we define a class using the class keyword and a structure using the struct keyword. Let's look at the syntax used to create both classes and structures:

class MyClass {
// MyClass definition
}

struct MyStruct {
// MyStruct definition
}

In the preceding code, we define a new class named MyClass and a new structure named MyStruct. This effectively creates two new Swift types named MyClass and MyStruct. When we name a new type, we want to use the standard naming convention set by Swift, where the name is in camel case with the first letter being uppercase. This is also known as PascalCase. Any method or property defined within the class or structure should also be named using camel case with the first letter being uppercase.

Access controls

Access controls enable us to hide implementation details and only expose the interfaces we want to expose. This feature is handled with access controls. We can assign specific access levels to both classes and structures. We can also assign specific access levels to properties, methods, and initializers that belong to our classes and structures.

In Swift, there are five access levels:

  • Open: This is the most visible access control level. It allows us to use the property, method, class, and so on anywhere we want to import the module. Basically, anything can use an item that has an access control level of open. Anything that is marked open can be subclassed or overidden by any item within the module they are defined in and any module that imports the module it is defined in. This level is primarily used by frameworks to expose the framework's public API. The open access control is only available to classes and members of a class.
  • Public: This access level allows us to use the property, method, class, and so on anywhere we want to import the module. Basically, anything can use an item that has an access control level of public. Anything that is marked public can be subclassed or overridden only by any item within the module they are defined in. This level is primarily used by frameworks to expose the framework's public API.
  • Internal: This is the default access level. This access level allows us to use the property, method, class, and so on in the module the item is defined in. If this level is used in a framework, it lets other parts of the framework use the item but code outside the framework will be unable to access it.
  • Fileprivate: This access control allows access to the properties and methods from any code within the same source file that the item is defined in.
  • Private: This is the least visible access control level. It only allows us to use the property, method, class, and so on, within extensions of that declaration defined in the source file that defines it.

There are some limitations with access controls, but these limitations are there to ensure that access levels in Swift follow a simple guiding principle - no entity can be defined in terms of another entity that has a lower (more restrictive) access level. What this means is we cannot assign a higher (less restrictive) access level to an entity when it relies on another entity that has a lower (more restrictive) access level.

The following examples demonstrate this principle:

  • We cannot mark a method as being public when one of the arguments or the return type has an access level of private, because external code would not have access to the private type
  • We cannot set the access level of a method or property to public when the class or structure has an access level of private, because external code would not be able to access the constructor when the class is private

Properties

Properties associate values with a class or a structure. There are two types of properties:

  • Stored properties: Stored properties will store variable or constant values as part of an instance of a class or structure. Stored properties can also have property observers that can monitor the property for changes and respond with custom actions when the value of the property changes.
  • Computed properties: These do not store a value themselves, but retrieve and possibly set other properties. The value returned by a computed property can also be calculated when it is requested.

A stored property is a variable or constant that is stored as part of an instance of a class or structure. These are defined with the var and let keywords just like normal variables and constants. In the following code, we will create a structure named MyStruct and a class named MyClass. The structure and the class both contain two stored properties, c and v.

The stored property c is a constant because it is defined with the let keyword, and v is a variable because it is defined with the var keyword. Let's look at the following code:

struct MyStruct {
    let c = 5
    var v = ""
}

class MyClass {
    let c = 5
    var v = ""
}

As we can see from the example, the syntax to define a stored property is the same for both classes and structures. Let's look at how we would create an instance of both the structure and class. The following code creates an instance of the MyStruct structure named myStruct and an instance of the MyClass class named myClass:

var myStruct = MyStruct()
var myClass = MyClass()

One of the differences between structures and classes is that, by default, a structure creates an initializer that lets us populate the stored properties when we create an instance of the structure. Therefore, we could also create an instance of MyStruct like this:

var myStruct = MyStruct(v: "Hello")

In the preceding example, the initializer is used to set the variable v, and the c constant will still contain the number 5 that is defined in the structures.

The order in which the parameters appear in the initializer is the order that we defined them.

To set or read a stored property, we use the standard dot syntax. Let's look at how we would set and read stored properties in Swift:

var x = myClass.c
myClass.v = "Howdy"

Computed properties are properties that do not have backend variables that are used to store the values associated with the property. The values of a computed property are usually computed when code requests it. You can think of a computed property as a function disguised as a property. Let's look at how we would define a read-only computed property:

var salaryWeek: Double {
    get{
        return self.salaryYear/52
    }
}

We can simplify the definition of the read-only computed property by removing the get keyword, as shown in the following example:

var salaryWeek: Double {
    return self.salaryYear/52
}

Computed properties are not limited to being read-only, we can also write to them. To enable the salaryWeek property to be writeable, we will add a setter method. The following example shows how we add a setter method that will set the salaryYear property, based on the value being passed into the salaryWeek property:

var salaryWeek: Double {
    get {
        return self.salaryYear/52
    }
    set (newSalaryWeek){
        self.salaryYear = newSalaryWeek*52
    }
}

Inheritance

The concept of inheritance is a basic object-oriented development concept. Inheritance allows a class to be defined as having a certain set of characteristics and then other classes can be derived from that class. The derived class inherits all of the features of the class it is inheriting from (unless the derived class overrides those characteristics) and then usually adds additional characteristics of its own.

With inheritance, we can create what is known as a class hierarchy. In a class hierarchy, the class at the top of the hierarchy is known as the base class and the derived classes are known as subclasses. We are not limited to only creating subclasses from a base class, we can also create subclasses from other subclasses. The class that a subclass is derived from is known as the parent or superclass. In Swift, a class can have only one parent class. This is known as single inheritance.

Subclasses can call and access the properties, methods, and subscripts of their superclass. They can also override the properties, methods, and subscripts of their superclass. Subclasses can add property observers to properties that they inherit from a superclass, so they can be notified when the values of the properties change. Let's look at an example that illustrates how inheritance works in Swift.

We will start off by defining a base class named Plant. The Plant class will have two properties, height and age. It will also have one method, growHeight(). The height property will represent the height of the plant, the age property will represent the age of the plant, and the growHeight() method will be used to increase the height of the plant. Here is how we would define the Plant class:

class Plant {
    var height = 0.0
    var age = 0
    func growHeight(inches: Double) {
        height += inches;
    }
}

Now that we have our Plant base class, let's see how we would define a subclass of it. We will name this subclass Tree. The Tree class will inherit the age and height properties of the Plant class and add one more property named limbs. It will also inherit the growHeight() method of the Plant class and add two more methods: limbGrow(), where new limbs are grown, and limbFall(), where limbs fall off the tree. Let's have a look at the following code:

class Tree: Plant
    var limbs = 0

    func limbGrow()
        self.limbs += 1
    }

    func limbFall()
        self.limbs -= 1
    }
}

We indicate that a class has a superclass by adding a colon and the name of the superclass to the end of the class definition. In this example, we indicated that the Tree class has a superclass named Plant. Now, let's look at how we could use the Tree class that inherited the age and height properties from the Plant class:

var tree = Tree()
tree.age = 5
tree.height = 4
tree.limbGrow()
tree.limbGrow()

Protocols

There are times when we would like to describe the implementations (methods, properties, and other requirements) of a type without actually providing any implementation. For this, we can use protocols.

Protocols define a blueprint of methods, properties, and other requirements for a class or a structure. A class or a structure can then provide an implementation that conforms to those requirements. The class or structure that provides the implementation is said to conform to the protocol.

The syntax to define a protocol is very similar to how we define a class or a structure. The following example shows the syntax used to define a protocol:

protocol MyProtocol {
    //protocol definition here
}

We state that a class or structure conforms to a protocol by placing the name of the protocol after the type's name, separated by a colon. Here is an example of how we would state that a structure conforms to the MyProtocol protocol:

struct MyStruct: MyProtocol {
    //class implementation here
}

A type can conform to multiple protocols. We list the protocols that the type conforms to by separating them with commas. The following example shows how we would state that our structure conforms to multiple protocols:

struct MyStruct: MyProtocol, AnotherProtocol, ThirdProtocol {
    // class implementation here
}

If we need a class to both inherit from a superclass and implement a protocol, we would list the superclass first, followed by the protocols. The following example illustrates this:

class MyClass: MySuperClass, MyProtocol, MyProtocol2 {
    // Class implementation here
}

A protocol can require that the conforming type provides certain properties with a specified name and type. The protocol does not say if the property should be a stored or computed property because the implementation details are left up to the conforming type.

When defining a property within a protocol, we must specify whether the property is a read-only or a read-write property by using the get and set keywords. Let's look at how we would define properties within a protocol by creating a protocol named FullName:

protocol FullName {
    var firstName: String { get set }
    var lastName: String { get set }
}

The FullName protocol defines two properties, which any type that conforms to the protocol must implement. These are the firstName and lastName properties and both are read-write properties. If we wanted to specify that the property is read-only, we would define it with only the get keyword, like this:

var readOnly: String { get }

A protocol can require that the conforming class or structure provides certain methods. We define a method within a protocol exactly as we do within a class or structure, except without the curly braces or method body. Let's add a getFullName() method to FullName protocol and Scientist class.

protocol FullName {
    var firstName: String { get set }
    var lastName: String { get set }
    func getFullName() -> String
}

Now, we will need to add a getFullName() method to Scientist class so that it will conform to the protocol:

class Scientist: FullName {
    var firstName = ""
    var lastName = ""
    var field = ""

    func getFullName() -> String {
        return "\(firstName) \(lastName) studies \(field)"
    }
}

Associated types with protocols

When defining a protocol, there are times when it is useful to define one or more associated types. An associated type gives us a placeholder name that we can use within the protocol in place of a type. The actual type to use for the associated type is not defined until the protocol is adopted. The associated type basically says: we don't know the exact type to use; therefore, when a type adopts this protocol, it will define it. As an example, if we were to define a protocol for a queue, we would want the type that adopts the protocol to define the instance types that the queue contains rather than the protocol.

To define an associated type, we use the associatedtype keyword. Let's see how we can use associated types within a protocol. In this example, we will illustrate the Queue protocol, which will define the requirements that are needed to implement a queue:

protocol Queue  { 
    associatedtype QueueType 
    mutating func addItem(item: QueueType) 
    mutating func getItem() -> QueueType?  
    func count() -> Int 
} 

In this protocol, we define one associated type named QueueType. We then use this associated type twice within the protocol. First, we use it as the parameter type for the addItem() method, and then we use it again when we define the return type of the getItem() method as an optional type.

Any type that implements the Queue protocol must specify the type to use for the QueueType placeholder, and must also ensure that only items of that type are used where the protocol requires the QueueType placeholder.

Let's look at how to implement Queue in a non-generic class called IntQueue. This class will implement the Queue protocol using the integer type:

struct IntQueue: Queue  { 
    var items = [Int]() 
    mutating func addItem(item: Int) { 
        items.append(item) 
    } 
    mutating func getItem() -> Int?  { 
        if items.count > 0 { 
            return items.remove(at:  0) 
        } 
        else { 
            return  nil 
        } 
    } 
    func count() -> Int { 
        return items.count 
    } 
} 

As we can see in the IntQueue structure, we use the integer type for both the parameter type of the addItem() method and the return type of the getItem() method. In this example, we implemented the Queue protocol in a non-generic way. Generics in Swift allow us to define the type to use at runtime rather than compile time.

The heap vs. the stack

When you create a reference type such as class, the system stores the actual instance in a region of memory known as the heap. Instances of a value type such as a struct resides in a region of memory called the stack, unless the value is part of a class instance, in which case the value is stored on the heap with the rest of the class instance.

Both the heap and the stack have essential roles in the execution of any program. A general understanding of what they are and how they work will help you visualize the functional differences between a class and a structure:

  • The system uses the stack to store anything on the immediate thread of execution; it is tightly managed and optimized by the CPU. When a function creates a variable, the stack stores that variable and then destroys it when the function exits. Since the stack is so strictly organized, it’s very efficient, and thus quite fast.
  • The system uses the heap to store instances of reference types. The heap is generally a large pool of memory from which the system can request and dynamically allocate blocks of memory. Lifetime is flexible and dynamic. The heap doesn’t automatically destroy its data like the stack does; additional work is required to do that. This makes creating and removing data on the heap a slower process, compared to on the stack.
  • When you create an instance of a class, your code requests a block of memory on the heap to store the instance itself.
  • When you create an instance of a struct (that is not part of an instance of a class), the instance itself is stored on the stack, and the heap is never involved.

Structures vs. classes recap

Structures

  • Useful for representing values.
  • Implicit copying of values.
  • Becomes completely immutable when declared with let.
  • Fast memory allocation (stack).

Classes

  • Useful for representing objects with an identity.
  • Implicit sharing of objects.
  • Internals can remain mutable even when declared with let.
  • Slower memory allocation (heap).

The biggest reason for using structures (value types) over classes is the performance gain we get. Value types do not incur the additional overhead for reference counting that reference types incur. Value types are also stored on the stack, which provides better performance as compared to reference types, which are stored on the heap. It is also worth noting that copying values is relatively cheap in Swift.

Keep in mind that, as our value types get large, the performance cost of copying can negate the other performance gains of value types. In the Swift standard library, Apple has implemented copy-on-write behavior to reduce the overhead of copying large value types.