A generic is very similar to a type alias. The difference is that the exact type of a generic is determined by the context in which it is being used, instead of being determined by the implementing types. This also means that a generic only has a single implementation that must support all possible types. Let's start by defining a generic function.
In Chapter 5, A Modern Paradigm – Closures and Functional Programming, we created a function that helped us find the first number in an array of numbers that passes a test:
This would be great if we only ever dealt with arrays of integers
, but clearly it would be helpful to be able to do this with other types. In fact, dare I say, all types? We achieve this very simply by making our function generic. A generic function is declared similar to a normal function, but you include a list of comma-separated placeholders inside angled brackets (<>
) at the end of the function name, as shown:
We use this function similarly to any other function, as shown:
So what happens if the type we use in our closure doesn't match the type of array we pass in?
As you can see, we get an error that the types don't match.
You may have noticed that we have actually used generic functions before. All of the built in functions we looked at in Chapter 5, A Modern Paradigm – Closures and Functional Programming, such as map
and filter
are generic; they can be used with any type.
We have even experienced generic types before. Arrays and dictionaries are also generic. The Swift team didn't have to write a new implementation of array and dictionary for every type that we might want to use inside the containers; they created them as generic types.
Similar to a generic function, a generic type is defined just like a normal type but it has a list of placeholders at the end of its name. Earlier in this chapter, we created our own containers for strings and integers
. Let's make a generic version of these containers, as shown:
One interesting case to consider is if we try to initialize a bag with an empty array:
This is great because not only can the compiler determine the generic placeholder types based on the variables we pass to them, it can also determine the type based on how we are using the result.
Before we jump right into solving the problem, let's take a look at a simpler form of type constraints.
Let's say that we want to write a function that can determine the index of an instance within an array using an equality check. Our first attempt will probably look similar to the following code:
A type constraint looks similar to a type implementing a protocol using a colon (:
) after a placeholder name. Now, the compiler is satisfied that every possible type can be compared using the equality operator. If we were to try to call this function with a type that is not equatable, the compiler would produce an error:
This is another case where the compiler can save us from ourselves.
Now the compiler is happy and we can start to use our Bag2
struct with any type that is Hashable
. We are close to solving our Container
problem, but we need a constraint on the type alias of Container
, not Container
itself. To do that, we can use a where
clause.
You can specify any number of where
clauses you want after you have defined each placeholder type. They allow you to represent more complicated relationships. If we want to write a function that can check if our container contains a particular value, we can require that the element type is equatable: