Swift Generics Basics
Introduction
Generics are one of the most powerful features in Swift, allowing you to write flexible, reusable code that works with any type while maintaining Swift's type safety. Instead of writing functions or types that work with specific types, generics let you define code templates that can operate on any type.
In this tutorial, you'll learn:
- What generics are and why they're useful
- How to write generic functions
- How to create generic types
- Type constraints in generics
- Practical applications of generics in everyday coding
Why Use Generics?
Before diving into generics, let's understand why they're so valuable:
- Code Reusability: Write a function once, use it with multiple types
- Type Safety: Maintain Swift's strong type checking at compile time
- Performance: No need for type casting or runtime checks
- API Design: Create flexible, expressive APIs
Without generics, you'd need to duplicate code for different types or use techniques that sacrifice type safety.
Generic Functions
Let's start with a simple example. Imagine you want to swap two integers:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
var firstInt = 42
var secondInt = 99
swapTwoInts(&firstInt, &secondInt)
print("After swapping: firstInt = \(firstInt), secondInt = \(secondInt)")
// Output: After swapping: firstInt = 99, secondInt = 42
But what if you want to swap two strings? Or doubles? You'd need to write nearly identical functions:
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}
This duplicated code is inefficient. Here's where generics come in:
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
The <T>
syntax introduces a type parameter named T
. This function works with any type:
var firstInt = 42
var secondInt = 99
swapTwoValues(&firstInt, &secondInt)
print("After swapping: firstInt = \(firstInt), secondInt = \(secondInt)")
// Output: After swapping: firstInt = 99, secondInt = 42
var firstString = "hello"
var secondString = "world"
swapTwoValues(&firstString, &secondString)
print("After swapping: firstString = \(firstString), secondString = \(secondString)")
// Output: After swapping: firstString = world, secondString = hello
Breaking Down Generic Functions
Let's understand what's happening:
<T>
defines a placeholder typeT
- This placeholder is used in the function parameters
(_ a: inout T, _ b: inout T)
- When you call the function, Swift infers what
T
is based on the arguments - Swift ensures both arguments are of the same type
You can name the placeholder anything, but convention uses single uppercase letters (T, U, V) or descriptive names like Element
or KeyType
.
Generic Types
Just as functions can be generic, Swift lets you create generic types - custom classes, structures, and enumerations that can work with any type.
Let's create a generic stack (last-in, first-out collection):
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
return items.isEmpty ? nil : items.removeLast()
}
var isEmpty: Bool {
return items.isEmpty
}
var count: Int {
return items.count
}
func peek() -> Element? {
return items.last
}
}
Now you can create stacks of any type:
// Integer stack
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
intStack.push(3)
if let lastItem = intStack.pop() {
print("Popped: \(lastItem)") // Output: Popped: 3
}
// String stack
var stringStack = Stack<String>()
stringStack.push("Swift")
stringStack.push("Generics")
stringStack.push("Are")
stringStack.push("Awesome")
while !stringStack.isEmpty {
if let word = stringStack.pop() {
print(word)
}
}
// Output:
// Awesome
// Are
// Generics
// Swift
Type Constraints
Sometimes you want to restrict the types that can be used with your generic code. Type constraints specify that a type parameter must inherit from a specific class or conform to a protocol.
Protocol Constraints
Let's create a function that finds the index of a value in an array, but only for types that can be compared for equality:
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
The T: Equatable
constraint means this function only works with types that conform to the Equatable
protocol (which allows the ==
operator).
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let index = findIndex(of: "llama", in: strings) {
print("Found llama at index \(index)") // Output: Found llama at index 2
}
let numbers = [1, 2, 3, 4, 5]
if let index = findIndex(of: 3, in: numbers) {
print("Found 3 at index \(index)") // Output: Found 3 at index 2
}
Multiple Constraints
You can apply multiple constraints using a where
clause:
func processItems<T, U>(items: [T], with specialValue: U)
where T: CustomStringConvertible, U: Numeric {
for item in items {
print("Processing \(item.description) with value \(specialValue)")
}
}
class MyItem: CustomStringConvertible {
let name: String
var description: String { return "Item: \(name)" }
init(name: String) {
self.name = name
}
}
let myItems = [MyItem(name: "First"), MyItem(name: "Second")]
processItems(items: myItems, with: 42)
// Output:
// Processing Item: First with value 42
// Processing Item: Second with value 42
Practical Examples
Generic Collection Functions
Let's create a function that transforms each element in a collection:
func transform<T, U>(_ items: [T], using transformer: (T) -> U) -> [U] {
var result: [U] = []
for item in items {
result.append(transformer(item))
}
return result
}
// Convert integers to strings
let numbers = [1, 2, 3, 4, 5]
let stringNumbers = transform(numbers) { "\($0)" }
print(stringNumbers) // Output: ["1", "2", "3", "4", "5"]
// Calculate squares
let squares = transform(numbers) { $0 * $0 }
print(squares) // Output: [1, 4, 9, 16, 25]
Result Type
A common use of generics is for success/failure results:
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
struct NetworkError: Error {
let message: String
}
func fetchData(from url: String, completion: @escaping (Result<String, NetworkError>) -> Void) {
// Simulate network request
if url.contains("example.com") {
completion(.success("Data successfully fetched"))
} else {
completion(.failure(NetworkError(message: "Invalid URL")))
}
}
fetchData(from: "https://example.com/data") { result in
switch result {
case .success(let data):
print("Success: \(data)")
case .failure(let error):
print("Error: \(error.message)")
}
}
// Output: Success: Data successfully fetched
Generic Cache
Here's a simple cache implementation using generics:
class Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
func setValue(_ value: Value, forKey key: Key) {
storage[key] = value
}
func getValue(forKey key: Key) -> Value? {
return storage[key]
}
func removeValue(forKey key: Key) {
storage.removeValue(forKey: key)
}
var count: Int {
return storage.count
}
}
// String to Int cache
let numberCache = Cache<String, Int>()
numberCache.setValue(42, forKey: "answer")
numberCache.setValue(7, forKey: "lucky")
if let answer = numberCache.getValue(forKey: "answer") {
print("The answer is \(answer)") // Output: The answer is 42
}
// String to URL cache
let urlCache = Cache<String, URL>()
urlCache.setValue(URL(string: "https://apple.com")!, forKey: "apple")
urlCache.setValue(URL(string: "https://swift.org")!, forKey: "swift")
if let swiftURL = urlCache.getValue(forKey: "swift") {
print("Swift URL is \(swiftURL)") // Output: Swift URL is https://swift.org
}
Summary
Swift generics allow you to write flexible, reusable code while maintaining type safety. Key takeaways:
- Generics let you write functions and types that work with any type
- Type parameters are defined using angle brackets:
<T>
- Type constraints restrict which types can be used with your generic code
- Swift infers the concrete type at compile time when you use generic code
- Generics help reduce code duplication while preserving type safety
Generics are foundational to Swift's standard library, with types like Array<Element>
, Dictionary<Key, Value>
, and Optional<Wrapped>
all being generic types.
Additional Resources
- Swift Documentation on Generics
- Apple's Swift Programming Language Guide
- WWDC Session: Swift Generics (Advanced)
Exercises
- Create a generic
Queue<T>
struct similar to ourStack<Element>
example - Write a generic function that filters an array based on a condition
- Create a
Pair<T, U>
struct that holds two values of potentially different types - Implement a generic binary search function with the constraint that the elements must be comparable
- Create a simple generic
Result<Success, Failure>
type that includes additional helper methods
Happy coding with generics!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)