Skip to main content

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:

  1. Code Reusability: Write a function once, use it with multiple types
  2. Type Safety: Maintain Swift's strong type checking at compile time
  3. Performance: No need for type casting or runtime checks
  4. 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:

swift
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:

swift
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:

swift
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:

swift
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:

  1. <T> defines a placeholder type T
  2. This placeholder is used in the function parameters (_ a: inout T, _ b: inout T)
  3. When you call the function, Swift infers what T is based on the arguments
  4. 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):

swift
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:

swift
// 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:

swift
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).

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

  1. Generics let you write functions and types that work with any type
  2. Type parameters are defined using angle brackets: <T>
  3. Type constraints restrict which types can be used with your generic code
  4. Swift infers the concrete type at compile time when you use generic code
  5. 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

Exercises

  1. Create a generic Queue<T> struct similar to our Stack<Element> example
  2. Write a generic function that filters an array based on a condition
  3. Create a Pair<T, U> struct that holds two values of potentially different types
  4. Implement a generic binary search function with the constraint that the elements must be comparable
  5. 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! :)