Associated Types

When defining a protocol, it is sometimes useful to declare one or more associated types as part of the protocol's definition. An associated type gives a placeholder name (or alias) to a type that is used as part of the protocol. The actual type to use for that associated type is not specified until the protocol is adopted. Associated types are specified with the typealias keyword.

Associated Types in Action

Here's an example of a protocol called Container, which declares an associated type called ItemType:

protocol Container {
    typealias ItemType
    mutating func append(item: ItemType)
    var count: Int {get}
    subscript(i: Int)->ItemType { get }
}

The Container protocol defines three required capabilities that any container must provide:

  • It must be possible to add a new item to the container with an `append(_:) method
  • It must be possible to access a count of the items in the container through a count property that returns an Int value.
  • It must be possible to retrieve each item in the container with a subscript that takes an Int index value.

This protocol doesn't specify how the items in the container should be stored or what type they are allowed to be. The protocol only specifies the three bits of functionality that any type must provide in order to be considered a Container. A conforming type can provide additional functionality, as long as it satisfies these three requirements.

Any type that conforms to the Container protocol must be able to specify the type of values it stores. Specifically, it must ensure that only items of the right type are added to the container, and it must be clear about the type of the items returned by its subscript.

To define these requirements, the Container protocol needs a way to refer to the type of the elements that a container will hold, without knowing what that type is for a specific container. The Container protocol needs to specify that any value passed to the append(_:) method must have the same type as the container's element type, and that the value returned by the container's subscript will be of the same type as the container's element type.

To achieve this, the Container protocol declares an associated type called ItemType, written as typealias ItemType. The protocol does not define what ItemType is an alias for - that information is left for any conforming type to provide. Nontheless, the ItemType alias provides a way to refer to the type of the items in a Container, and to define a type for use with the append(_:) method and subscript, to ensure that the expected behavior of any Container is enforced.

Here's a version of the non-generic IntStack type from earlier, adapted to conform to the Container protocol:

struct IntStack: Container {
    // original IntStack implementation
    var items = [Int]()
    mutating func push(item: Int) {
        items.append(item)
    }
    mutating func pop()->Int {
        return items.removeLast()
    }

    // conformance to the Container protocol
    typealias ItemType = Int
    mutating func append(item: Int) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int)->Int {
        return items[i]
    }
}

The IntStack type implements all three of the Container protocol's requirements, and in each case wraps part of the IntStack type's existing functionality to satisfy these requirements.

Moreover, IntStack specifies that for this implementation of Container, the appropriate ItemType to use is a type of Int. The definition of typealias ItemType = Int turns the abstract type of ItemType into a concrete type of Int for this implementation of the Container protocol.

Thanks to Swift's type inference, you don't actually need to declare a concrete ItemType of Int as part of the definition of IntStack. Because IntStack conforms to all of the requirements of the Container protocol, Swift can infer the appropriate ItemType to use, simply by looking at the type of the append(_:) method's item parameter and the return type of the subscript. Indeed, if you delete the typealias ItemType = Int line from the code above, everything still works, because it is clear what type should be used for ItemType.

You can also make the generic Stack type conform to the Container protocol:

struct Stack: Container {
    // original Stack implementation
    var items = [Element]()
    mutating func push(item: Element) {
        items.append(item)
    }
    mutating func pop()->Element {
        return items.removeLast()
    }
    // conformance to the Container protocol
    mutating func append(item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int)->Element {
        return items[i]
    }
}

This time, the type parameter Element is used as the type of the append(_:) method'sitemparameter and the return type of the subscript. Swift can therefore infer thatElementis the appropriate type to use as theItemType` for this particular container.

Extending an Existing Type to Specify an Associated Type

You can extend an existing type to add conformance to a protocol. This includes a protocol with an associated type.

Swift's Array type already provides an append(_:) method, and a subscript with an Int index to retrieve its elements. These three capabilities match the requirements of the Container protocol. This means that you can extend Array to conform to the Container protocol simply by declaring that Array adopts the protocol. You do this with an empty extension.
extension Array: Container { }

Array's existing append(_:) method and subscript enable Swift to infer the appropriate type to use for ItemType, just as for the generic Stack type above. After defining this extension, you can use any Array as a Container.

你可能感兴趣的:(Associated Types)