Protocols, like other types, can be extended. Protocol extensions can be used to provide common functionality to all types that conform to a particular protocol. This gives us the ability to add functionality to any type that conforms to a protocol rather than adding the functionality to each individual type or through a global function. Protocol extensions, like regular extensions, also give us the ability to add functionality to types that we do not have the source code for.
Protocol-oriented programming and frameworks such as GameplayKit rely heavily on protocol extensions. Without protocol extensions, if we wanted to add specific functionality to a group of types that conformed to a protocol, we would have to add the functionality to each of the types. If we were using reference types (classes), we could create a class
hierarchy, but, as we mentioned earlier, that is not possible with value types. Apple has stated that we should prefer value types to reference types and with protocol extensions we have the ability to add common functionality to a group of values and/or reference types that conform to a specific protocol, without having to implement that functionality in all types.
Let's see what protocol extensions can do for us. The Swift standard library provides a protocol named Collection:
http://swiftdoc.org/nightly/protocol/Collection/
This protocol inherits from the Sequence protocol and it is adopted by all of Swift's standard collection types such as the Dictionary and Array.
Let's say that we wanted to add the functionality to all types that conform to the Collection protocol. This new functionality would shuffle the items in a collection or return only the items whose index number is an even number. We could very easily add this functionality by extending the Collection protocol, as shown in the following code:
extension Collection {
func evenElements() -> [Iterator.Element] { var index = startIndex
var result: [Iterator.Element] = [] var i = 0
repeat {
if i % 2 == 0 {
result.append(self[index]) }
index = self.index(after: index) i += 1
} while (index != endIndex) return result
}
func shuffle() -> [Iterator.Element] { return sorted(){ left, right in return arc4random() < arc4random() }
} }
Notice that when we extend a protocol, we use the same syntax and format that we use when we extend other types. We use the extension keyword followed by the name of the protocol that we are extending. We then put the functionality we are adding between the curly brackets. Now every type that conforms to the Collection protocol will receive both the evenElements() and shuffle() functions. The following code shows how we would use these functions with an array:
var origArray = [1,2,3,4,5,6,7,8,9,10] var newArray = origArray.evenElements() var ranArray = origArray.shuffle()
In the previous code, the newArray array will contain the elements 1, 3, 5, 7, and 9 because those elements have index numbers that are even (we are looking at the index number, not the value of the element). The ranArray array will contain the same elements as the origArray, but the order will be shuffled.
Protocol extensions are great for adding functionality to a group of types without the need to add the code to each of the individual types; however, it is important to know what types conform to the protocol we are extending. In the previous example, we extended the
Collection protocol by adding the evenElements() and shuffle() methods to all types that conform to the protocol. One of the types that conforms to this protocol is the Dictionary type. However, the Dictionary type is an unordered collection; therefore, the evenElements() method will not work as expected. The following example illustrates this:
var origDict = [1:"One",2:"Two",3:"Three",4:"Four"] var returnElements = origDict.evenElements()
for item in returnElements { print(item)
}
Since the Dictionary type does not promise to store the items in any particular order, any of the two items could be printed to the screen in this example. The following shows one possible output from this code:
(2, "two") (1, "One")
Another problem is that anyone who is not familiar with how the evenElements() method is implemented may expect the returnElements array to be of the Dictionary type since the original collection is a Dictionary type; however, it is actually an instance of the Array type. This can cause some confusion. Therefore, we need to be careful when we extend a protocol to make sure the functionality we are adding works as expected for all types that conform to the protocol. In the case of the shuffle() and evenElements() methods, we might have been better served to add the functionality as an extension directly to the Array type rather than the Collection protocol, however there is another way. We can add constraints to our extensions that will limit the types that receive the functionality defined in an extension.
In order for a type to receive the functionality defined in a protocol extension, it must satisfy all constraints defined within the protocol extension. A constraint is added after the name of the protocol that we are extending using the where keyword. The following example shows how we would add a constraint to our Collection extension:
extension Collection where Self: ExpressibleByArrayLiteral { //Extension code here
}
In the Collection protocol extensions in the previous example, only types that also conform to the ExpressibleByArrayLiteral protocol
(http://swiftdoc.org/nightly/protocol/ExpressibleByArrayLiteral/) will receive the
functionality defined in the extension. Since the Dictionary type does not conform to the ExpressibleByArrayLiteral protocol, it will not receive the functionality defined within the protocol extension.
We could also use constraints to specify that our Collection protocol extension only applies to a collection whose elements conform to a specific protocol. In the following example, we use constraints to make sure that the elements in the collection conform to the Comparable protocol. This may be necessary if the functionality that we are adding relies on the ability to compare two or more elements in the collection. We could add this constraint as follows:
extension Collection where Iterator.Element: Comparable { // Add functionality here
}
Constraints give us the ability to limit which types receive the functionality defined in the extension. One thing that we need to be careful of is using protocol extensions when we should actually be extending an individual type. Protocol extensions should be used when we want to add functionality to a group of types. If we are trying to add the functionality to a single type, we should look to extend that individual type.
Now that we have seen how to use extensions and protocol extensions, let's look at a real- world example. In this example, we will show how to create a text validation framework.