Associated types with protocols

When defining a protocol, there are times where 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 do not 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 typealias keyword. Let's see how to use associated types within a protocol. In this example, we will define the QueueProtocol protocol that will define the requirements needed to implement a queue:

protocol QueueProtocol {
    typealias 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, once as the parameter type for the addItem() method. We then use it again when we define the return type of the getItem() method as an optional type that might return an instance defined by the QueueType associated type; alternatively, it might return nil.

Any type that implements the QueueProtocol 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 uses the QueueType placeholder.

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

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

As we can see in the IntQueue structure, we use the Int type for both the parameter type of the addItem() method and the return type of the getItem() method. These are the same types that were defined as the QueueType typealias in the QueueProtocol protocol.

In the preceding example, we implemented the QueueProtocol protocol in a non-generic way. Generics in Swift allow us to define the type to use at runtime rather than compile time. Let's see how to implement the QueueProtocol protocol as a generic type called GenericQueue:

class GenericQueue<T>: QueueProtocol {
    var items = [T]()
    
    func addItem(item: T) {
        items.append(item)
    }
    
    func getItem() -> T? {
        if items.count > 0 {
            return items.removeAtIndex(0)
        } else {
            return nil
        }
    }
    
    func count() -> Int {
        return items.count
    }
}

The GenericQueue implementation is very similar to the IntQueue implementation, except that we define the type to use with the generic placeholder T. We can then use the GenericQueue class as we would use any generic type. Let's take a look at how to use the GenericQueue class:

var intQ2 = GenericQueue<Int>()
intQ2.addItem(2)
intQ2.addItem(4)
print(intQ2.getItem())
intQ2.addItem(6)

Now that we have explored protocols in some detail, let's look at how we would implement the delegation design pattern using protocols.