Type constraints
In your definition of Keeper, the identifier Animal serves as a type parameter, which is a named placeholder for some actual type that will be supplied later.
This is much like the parameter cat in a simple function like func feed(cat: Cat) {
/ open can, etc... / }. But when defining a function, you can’t simply pass any argument to the function. You can only pass values of type Cat.
At present, you could offer any type at all as the kept Animal, even something nonsensically unlike an animal, like a String or Int. This is no good. What you’d like is something analogous to a function, something where you can restrict what kinds of types are allowed to fill the type parameter. In Swift, you do this with type constraints.
There are two kinds of constraints. The simplest kind of type constraint looks like this:
class Keeper<Animal: Pet> {
/ definition body as before /
}
The identifier Pet must name a type or a protocol. The new expression within the type parameter brackets, Animal: Pet, specifies the requirement that whatever type is offered to be Animal must meet the requirement of either being a subclass of Pet or of implementing the Pet protocol.
For instance, you can enforce these restrictions by using the revised Keeper definition above, and also redefine Cat and other animals to implement a Pet or retro-actively model conformance to the protocol using an extension.
protocol Pet {
var name: String { get } // all pets respond to a name
}
extension Cat: Pet {} extension Dog: Pet {}
This works because Cat and Dog already implement a name strored property. Next create arrays of cats and dogs like so:
let cats = ["Miss Gray", "Whiskers"].map { Cat(name: $0) } let dogs = ["Sparky", "Rusty", "Astro"].map { Dog(name: $0) } let pets: [Pet] = [Cat(name: "Mittens"), Dog(name: "Yeller")]
Notice that the cats, dogs, and pets all have different types. If you option-click on each one you can see the types are [Cat], [Dog], and [Pet] respectively. The cats and dogs array are said to be homogeneous because the elements have the same type. The constant pets is heterogenus because it is a mix of dog and cat types.
By constraining a type, you can operate on it. For example, since you now know that pets respond to name you can write a set of generic functions that herd pets. Write four different flavors of a herd functions like so:
// 1
func herd(_ pets: [Pet]) { pets.forEach {
print("Come ($0.name)!")
}
}
// 2
func herd<Animal: Pet>(_ pets: [Animal]) { pets.forEach {
print("Here ($0.name)!")
}
}
// 3
func herd<Animal: Cat>(_ cats: [Animal]) { cats.forEach {
print("Here kitty kitty. Here ($0.name)!")
}
}
// 4
func herd<Animal: Dog>(_ dogs: [Animal]) { dogs.forEach {
print("Here ($0.name)! Come here!")
}
}
herd(dogs) herd(cats) herd(pets)
- This method handles a array of type Pet that can mix Dog and Cat elements together.
- This method handles arrays of any kind of Pet but they need to be all of a single type. The angle brackets after the function name let you specify the generic type to be used in the function.
- Handles cats and only cats (or subtypes of cats).
- Handles dogs and only dogs (or subtypes of dogs).
Swift binds the function call to the most specific call that it can.
The second kind of type constraint involves making explicit assertions that a type parameter, or its associated type, must equal another parameter or one of its conforming types. By combining many type parameters and sets of requirements relating their associated types, one can define a complex relationships on top of generic types.
To demostrate this, suppose you want to add an extension to arrays of Cats that adds the method meow(). Element is the associated type of an Array. You can require that Element be of type (or subtype of) Cat:
extension Array where Element: Cat { func meow() {
forEach { print("($0.name) says meow!") }
}
}
Now you can call the meow method:
cats.meow()
On the other hand, this will not work with dogs:
dogs.meow() // error: 'Dog' is not a subtype of 'Cat'
These mistakes may seem obvious here but type constraints can prevent you from making subtle mistakes at compile time.