Chapter 6. Make Swift Work For You – Protocols and Generics

As we learned in Chapter 2, Building Blocks – Variables, Collections, and Flow Control, Swift is a strongly typed language, which means that every piece of data must have a type. Not only can we take advantage of this to reduce the clutter in our code, we can also leverage it to let the compiler catch bugs for us. The earlier we catch a bug, the better. Besides not writing them in the first place, the earliest place where we can catch a bug is when the compiler reports an error.

Two big tools that Swift provides to achieve this are called protocols and generics. Both of them use the type system to make our intentions clear to the compiler so that it can catch more bugs for us.

In this chapter, we will cover the following topics:

  • Protocols
  • Generics
  • Extending existing generics
  • Extending protocols
  • Putting protocols and generics to use

Protocols

The first tool we will look at is protocols. A protocol is essentially a contract that a type can sign, specifying that it will provide a certain interface to other components. This relationship is significantly looser than the relationship a subclass has with its superclass. A protocol does not provide any implementation to the types that implement them. Instead, a type can implement them in any way that they like.

Let's take a look at how we define a protocol, in order to understand them better.

Defining a protocol

Let's say we have some code that needs to interact with a collection of strings. We don't actually care what order they are stored in and we only need to be able to add and enumerate elements inside the container. One option would be to simply use an array, but an array does way more than we need it to. What if we decide later that we would rather write and read the elements from the file system? Furthermore, what if we want to write a container that would intelligently start using the file system as it got really large? We can make our code flexible enough to do this in the future by defining a string container protocol, which is a loose contract that defines what we need it to do. This protocol might look similar to the following code:

protocol StringContainer {
    var count: Int { get }
    mutating func addString(string: String)
    func enumerateStrings(handler: (string: String) -> Void)
}

Predictably, a protocol is defined using the protocol keyword, similar to a class or a structure. It also allows you to specify computed properties and methods. You cannot declare a stored property because it is not possible to create an instance of a protocol directly. You can only create instances of types that implement the protocol. Also, you may notice that none of the computed properties or methods provide implementations. In a protocol, you only provide the interface.

Since protocols cannot be initialized on their own, they are useless until we create a type that implements them. Let's take a look at how we can create a type that implements our StringContainer protocol.

Implementing a protocol

A type "signs the contract" of a protocol in the same way that a class inherits from another class except that structures and enumerations can also implement protocols:

struct StringBag: StringContainer {
    // Error: Type 'StringBag' does not conform to protocol 'StringContainer'
}

As you can see, once a type has claimed to implement a specific protocol, the compiler will give an error if it has not fulfilled the contract by implementing everything defined in the protocol. To satisfy the compiler, we must now implement the count computed property, mutating function addString:, and function enumerateStrings: as they are defined. We will do this by internally holding our values in an array:

struct StringBag: StringContainer {
    var strings = [String]()
    var count: Int {
        return self.strings.count
    }
    
    mutating func addString(string: String) {
        self.strings.append(string)
    }
    
    func enumerateStrings(handler: (string: String) -> Void) {
        for string in self.strings {
            handler(string: string)
        }
    }
}

The count property will always just return the number of elements in our strings array. The addString: method can simply add the string to our array. Finally, our enumerateString: method just needs to loop through our array and call the handler with each element.

At this point, the compiler is satisfied that StringBag is fulfilling its contract with the StringContainer protocol.

Now, we can similarly create a class that implements the StringContainer protocol. This time, we will implement it using an internal dictionary instead of an array:

class SomeSuperclass {}
class StringBag2: SomeSuperclass, StringContainer {
    var strings = [String:Void]()
    var count: Int {
        return self.strings.count
    }
    
    func addString(string: String) {
        self.strings[string] = ()
    }
    
    func enumerateStrings(handler: (string: String) -> Void) {
        for string in self.strings.keys {
            handler(string: string)
        }
    }
}

Here we can see that a class can both inherit from a superclass and implement a protocol. The superclass always has to come first in the list, but you can implement as many protocols as you want, separating each one with a comma. In fact, a structure and enumeration can also implement multiple protocols.

With this implementation we are doing something slightly strange with the dictionary. We defined it to have no values; it is simply a collection of keys. This allows us to store our strings without any regard to the order they are in.

Now, when we create instances, we can actually assign any instance of any type that implements our protocol to a variable that is defined to be our protocol, just like we can with superclasses:

var someStringBag: StringContainer = StringBag()
someStringBag.addString("Sarah")
someStringBag = StringBag2()
someStringBag.addString("Sarah")

When a variable is defined with our protocol as its type, we can only interact with it using the interface that the protocol defines. This is a great way to abstract implementation details and create more flexible code. By being less restrictive on the type that we want to use, we can easily change our code without affecting how we use it. Protocols provide the same benefit that superclasses do, but in an even more flexible and comprehensive way, because they can be implemented by all types and a type can implement an unlimited number of protocols. The only benefit that superclasses provide over protocols is that superclasses share their implementations with their children.

Using type aliases

Protocols can be made more flexible using a feature called type aliases. They act as a placeholder for a type that will be defined later when the protocol is being implemented. For example, instead of creating an interface that specifically includes strings, we can create an interface for a container that can hold any type of value, as shown:

protocol Container {
    typealias Element
    
    mutating func addElement(element: Element)
    func enumerateElements(handler: (element: Element) -> Void)
}

As you can see, this protocol creates a type alias called Element using the keyword typealias. It does not actually specify a real type; it is just a placeholder for a type that will be defined later. Everywhere we have previously used a string, we simply refer to it as Element.

Now, we can create another string bag that uses the new Container protocol with a type alias instead of the StringContainer protocol. To do this, we not only need to implement each of the methods, we also need to give a definition for the type alias, as shown:

struct StringBag3: Container {
    typealias Element = String

    var elements = [Element:Void]()

    var count: Int {
        return elements.count
    }

    mutating func addElement(element: Element) {
        self.elements[element] = ()
    }

    func enumerateElements(handler: (element: Element) -> Void) {
        for element in self.elements.keys {
            handler(element: element)
        }
    }
}

With this code, we have specified that the Element type alias should be a string for this implementation using an equal sign (=). This code continues to use the type alias for all of the properties and methods, but you can also use string since they are in fact the same thing now.

Using the type alias actually makes it really easy for us to create another structure that can hold integers instead of strings:

struct IntBag: Container {
    typealias Element = Int

    var elements = [Element:Void]()

    var count: Int {
        return elements.count
    }

    mutating func addElement(element: Element) {
        self.elements[element] = ()
    }

    func enumerateElements(handler: (element: Element) -> Void) {
        for element in self.elements.keys {
            handler(element: element)
        }
    }
}

The only difference between these two pieces of code is that the type alias has been defined to be an integer in the second case instead of a string. We could use copy and paste to create a container of virtually any type, but as usual, doing a lot of copy and paste is a sign that there is a better solution. Also, you may notice that our new Container protocol isn't actually that useful on its own because with our existing techniques, we can't treat a variable as just a Container. If we are going to interact with an instance that implements this protocol, we need to know what type it has assigned the type alias to.

Swift provides a tool called generics to solve both of these problems.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset