A generic type is a class, structure, or enumeration that can work with any type, just like Swift arrays and optionals can work with any type. When we create an instance of our generic type, we specify the type that the instance will work with. Once a type is defined, it cannot be changed for that instance.
To demonstrate how to create a generic type, let's create a simple List class. This class will use a Swift array as the backend storage and will let us add items or retrieve values from the list.
Let's begin by seeing how to define our generic List type:
struct List<T> { }
The preceding code defines the generic List type. We can see that we use the <T> tag to define a generic placeholder, just like we did when we defined a generic function. This T placeholder can then be used anywhere within the type instead of a concrete type definition.
To create an instance of this type, we would need to define the type of items that our list will hold. The following examples show how to create instances of the generic List type for various types:
var stringList = List<String>() var intList = List<Int>()
The preceding example creates three instances of the List type. The stringList instance can be used with instances of the String type, the intList instance can be used with instances of the Integer type, and the customList instance can be used with instances of the MyObject type.
We are not limited to using generics only with structures. We can also define classes and enumerations as generic types. The following examples show how to define a generic structure and a generic enumeration:
class GenericStruct<T> { }
enum GenericEnum<T> { }
The next step in our List type is to add the backend storage array. The items that are stored in this array need to be of the same type as we define when we initiate the class, therefore we will use the T placeholder for the array's definition. The following code shows the List class with an array named items:
struct List<T> { var items = [T]() }
Now, we will need to add the add(item:) method that will be used to add an item to the list. We will use the T placeholder within the method declaration to define that the
parameter will be of the same type as we declared when we initiated the type. Therefore, if we create an instance of the List type to use the String type, we would be required to use the string type as the parameter for this method.
Here is the code for the add() function:
mutating func add(item: T) { items.append(item)
}
When we created a standalone generic function, we added the <T> declaration after the function name to declare that it is a generic function. When we use a generic method within a generic type, we do not need this declaration because we already specified that the type itself is generic with the T type. To define a generic method, within a generic type, all we need to do is to use the same placeholder that we defined in the type declaration.
Now, let's add the getItemAtIndex(index:) method that will return an item from the backend array, at the specified index:
func getItemAtIndex(index: Int) -> T? { if items.count > index { return items[index] } else { return nil } }
The getItemAtIndex(index:) method accepts one argument which is the index of the item we want to retrieve. We then use the T placeholder with the return type. The return type for this method is an optional that might be of type T or might be nil. If the backend storage array contains an item at the specified index, we will return that item, otherwise, we return nil .
Now, let's look at our entire generic list class:
struct List<T> { var items = [T]()
mutating func add(item: T) { items.append(item)
}
func getItemAtIndex(index: Int) -> T? { if items.count > index { return items[index] } else { return nil } } }
As we can see, we initially defined the generic T placeholder type in the structure's
declaration. We then used this placeholder type within the structure in three places. We use it as the type for our items array, as the parameter type for the add(index:) method, and as the value for the optional return type in the getItemAtIndex() method.
Now, let's look at how to use the List type. When we use a generic type, we define the type to be used within the instance between angle brackets. The following code shows how to use the List class to store String types:
var list = List<String>() list.add(item: "Hello") list.add(item: "World")
print(list.getItemAtIndex(index: 1))
In this code, we start off by creating an instance of the List type called list and define that it will store String types. We then use the add(index:) method twice to store two items in the list instance. Finally, we use the getItemAtIndex() method to retrieve the item at index number 1, which will display Optional(World) to the console.
At the end of this chapter we will look at the List type again and show how to design and develop a List type in a protocol oriented way with the Copy-on-write feature.
We can also define our generic types with multiple placeholder types, similar to how we use multiple placeholders in our generic methods. To use multiple placeholder types, we would separate them with commas. The following example shows how to define multiple placeholder types:
class MyClass<T,E>{ }
We then create an instance of the MyClass type that uses instances of the String and Integer types, like this:
var mc = MyClass<String, Int>()
Type constraints can also be used with generic types. Once again, using a type constraint for a generic type is exactly the same as using one with a generic function. The following code shows how to use a type constraint to ensure that the generic type conforms to the
comparable protocol:
struct MyStruct<T: Comparable>{}
So far, in this chapter, we have seen how to use placeholder types with functions and types; however, this book is about protocol-oriented programming. When we declare generic types in a protocol, they are known as associated types.