Swift Generics Tutorial: Getting Started
Learn to write functions and data types while making minimal assumptions. Swift generics allow for cleaner code with fewer bugs. By Michael Katz.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Swift Generics Tutorial: Getting Started
25 mins
- Getting Started
- Other Examples of Swift Generics
- Arrays
- Dictionaries
- Optionals
- Results
- Writing a Generic Data Structure
- Writing a Generic Function
- Constraining a Generic Type
- Cleaning Up the Add Functions
- Extending a Generic Type
- Subclassing a Generic Type
- Enumerations With Associated Values
- Where to Go From Here?
Results
Result
is a new type in Swift 5. Like Optional
, it is a generic enum with two cases. Instead of something or nothing, a result is either a success or failure. Each case has its own associated generic type, success has a value and failure has an Error
.
Consider this case, where the royal magician recruits you to cast some spells. Known spells generate a symbol, but unknown spells fail. The function would look something like this:
enum MagicError: Error {
case spellFailure
}
func cast(_ spell: String) -> Result<String, MagicError> {
switch spell {
case "flowers":
return .success("💐")
case "stars":
return .success("✨")
default:
return .failure(.spellFailure)
}
}
Result
allows you to write functions that return a value or an error without having use try
syntax. As an added bonus, the generic specification of the failure case means that you don’t need to check the type as you would with a catch
block. If there’s an error, you can be certain there will be a MagicError
in the value associated with the .failure case.
Try out some spells to see Result
in action:
let result1 = cast("flowers") // .success("💐")
let result2 = cast("avada kedavra") // .failure(.spellFailure)
With a grasp of the basics of generics, you can learn about writing your own generic data structures and functions.
Writing a Generic Data Structure
A queue is a data structure kind of like a list or a stack, but one to which you can only add new values to the end (enqueue them) and only take values from the front (dequeue them). This concept might be familiar if you’ve ever used OperationQueue
— perhaps while making networking requests.
The Queen, happy with your efforts earlier in the tutorial, would now like you to write functionality to help keep track of royal subjects waiting in line to speak with her.
Add the following struct
declaration to the end of your playground:
struct Queue<Element> {
}
Queue
is a generic type with a type argument, Element
, in its generic argument clause. Another way to say this is, Queue
is generic over type Element
. For example, Queue<Int>
and Queue<String>
will become concrete types of their own at runtime, that can only enqueue and dequeue strings and integers, respectively.
Add the following property to the queue:
private var elements: [Element] = []
You’ll use this array to hold the elements, which you initialize as an empty array. Note that you can use Element
as if it’s a real type, even though it’ll be filled in later. You mark it as private
because you don’t want consumers of Queue
to access elements
. You want to force them to use methods to access the backing store.
Finally, implement the two main queue methods:
mutating func enqueue(newElement: Element) {
elements.append(newElement)
}
mutating func dequeue() -> Element? {
guard !elements.isEmpty else { return nil }
return elements.remove(at: 0)
}
Again, the type parameter Element
is available everywhere in the struct
body, including inside methods. Making a type generic is like making every one of its methods implicitly generic over the same type. You’ve implemented a type-safe generic data structure, just like the ones in the standard library.
Play around with your new data structure for a bit at the bottom of the playground, enqueuing waiting subjects by adding their royal id to the queue:
var q = Queue<Int>()
q.enqueue(newElement: 4)
q.enqueue(newElement: 2)
q.dequeue()
q.dequeue()
q.dequeue()
q.dequeue()
Have some fun by intentionally making as many mistakes as you can to trigger the different error messages related to generics — for example, add a string to your queue. The more you know about these errors now, the easier it will be to recognize and deal with them in more complex projects.
Writing a Generic Function
The Queen has a lot of data to process, and the next piece of code she asks you to write will take a dictionary of keys and values and convert it to a list.
Add the following function to the bottom of the playground:
func pairs<Key, Value>(from dictionary: [Key: Value]) -> [(Key, Value)] {
return Array(dictionary)
}
Take a good look at the function declaration, parameter list and return type.
The function is generic over two types that you’ve named Key
and Value
. The only parameter is a dictionary with a key-value pair of type Key
and Value
. The return value is an array of tuples of the form — you guessed it — (Key, Value)
.
You can use pairs(from:)
on any valid dictionary and it will work, thanks to generics:
let somePairs = pairs(from: ["minimum": 199, "maximum": 299])
// result is [("maximum", 299), ("minimum", 199)]
let morePairs = pairs(from: [1: "Swift", 2: "Generics", 3: "Rule"])
// result is [(1, "Swift"), (2, "Generics"), (3, "Rule")]
Of course, since you can’t control the order in which the dictionary items go into the array, you may see an order of tuple values in your playground more like “Generics”, “Rule”, “Swift”, and indeed, they kind of do! :]
At runtime, each possible Key
and Value
will act as a separate function, filling in the concrete types in the function declaration and body. The first call to pairs(from:)
returns an array of (String, Int)
tuples. The second call uses a flipped order of types in the tuple and returns an array of (Int, String)
tuples.
You created a single function that can return different types with different calls. You can see how keeping your logic in one place can simplify your code. Instead of needing two different functions, you handled both calls with one function.
Now that you know the basics of creating and working with generic types and functions, it’s time to move on to some more advanced features. You’ve already seen how useful generics are to limit things by type, but you can add additional constraints as well as extend your generic types to make them even more useful.
Constraining a Generic Type
Wishing to analyze the ages of a small set of her most loyal subjects, the Queen requests a function to sort an array and find the middle value.
When you add the following function to your playground:
func mid<T>(array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.sorted()[(array.count - 1) / 2]
}
You’ll get an error. The problem is that for sorted()
to work, the elements of the array need to be Comparable
. You need to somehow tell Swift that mid
can take any array as long as the element type implements Comparable
.
Change the function declaration to the following:
func mid<T: Comparable>(array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.sorted()[(array.count - 1) / 2]
}
Here, you use the :
syntax to add a type constraint to the generic type parameter T
. You can now only call the function with an array of Comparable
elements, so that sorted()
will always work! Try out the constrained function by adding:
mid(array: [3, 5, 1, 2, 4]) // 3
You’ve already seen this when using Result: The Failure
type is constrained to Error
.