How to use generics in Swift?

Swift Docs - Generics

Swift is a language that lets you write code in different ways. One of the ways is to use generics. Generics let you write code that can work with any type of data. Generics make your code reusable and flexible, without losing type safety or speed.

In this blog post, I will explain what are generics in Swift, how to use them in your code, and what are the benefits of using them. I will also show you how to use generics to create a stack data structure with different data types, such as Int and String.

What are generics in Swift?

Generics are a way to write code that can work with any type of data. You can use generics to write functions, types, or protocols that can handle different types of data without repeating yourself or using type casts.

For example, suppose you want to write a function that swaps the values of two variables. You could write something like this:

// define a function that swaps two integers
func swapInts(_ a: inout Int, _ b: inout Int) {
    // store the value of a in a temporary variable
    let temp = a
    // assign the value of b to a
    a = b
    // assign the value of temp to b
    b = temp
}

// create two integer variables
var x = 10
var y = 20

// call swapInts with x and y as arguments
swapInts(&x, &y)

// print x and y after swapping
print(x) // 20
print(y) // 10

In this example, we have defined a function called swapInts that swaps two integers. We have used the inout keyword to indicate that the parameters are passed by reference, meaning that they can be modified inside the function. We have also used the & operator to pass the variables as references. We have then called swapInts with two integer variables x and y as arguments, and printed their values after swapping.

However, this function only works with integers. What if we want to swap two strings, or two doubles, or two custom types? We would have to write separate functions for each type, which would be tedious and repetitive. Moreover, we would have to use type casts or conditional statements to check the types of the variables before swapping them, which would be error-prone and inefficient.

This is where generics come in handy. We can write a generic function that can swap any type of variables, without knowing their exact types beforehand. We can write something like this:

// define a generic function that swaps any type of variables
func swapValues<T>(_ a: inout T, _ b: inout T) {
    // store the value of a in a temporary variable
    let temp = a
    // assign the value of b to a
    a = b
    // assign the value of temp to b
    b = temp
}

// create two string variables
var s1 = "hello"
var s2 = "world"

// call swapValues with s1 and s2 as arguments
swapValues(&s1, &s2)

// print s1 and s2 after swapping
print(s1) // world
print(s2) // hello

// create two double variables
var d1 = 3.14
var d2 = 2.71

// call swapValues with d1 and d2 as arguments
swapValues(&d1, &d2)

// print d1 and d2 after swapping
print(d1) // 2.71
print(d2) // 3.14

In this example, we have defined a generic function called swapValues that swaps any type of variables. We have used the syntax to indicate that the function has a generic type parameter T. The T type represents the type of the variables that the function can swap. However, we don’t know what the T type is exactly; we leave it as a placeholder that will be decided by the types of the arguments when we call the function.

We have then called swapValues with different types of variables: strings, doubles, and custom types (not shown). We have used the same function for all types of variables, without repeating ourselves or using type casts. This shows how generics allow us to write code that can work with any type of data.

How to use generics in Swift to create a stack data structure with different data types?

A stack is a data structure that stores items in a last-in first-out (LIFO) order. You can add items to the top of the stack and remove items from the top of the stack. You can also check how many items are in the stack and if the stack is empty.

We can use generics to write a generic type that implements a stack data structure. We can write something like this:

// define a generic type that implements a stack data structure
struct Stack<Element> {
    // define a private array to store the elements
    private var elements = [Element]()
    
    // define a method to add an element to the top of the stack
    mutating func push(_ element: Element) {
        elements.append(element)
    }
    
    // define a method to remove and return an element from the top of the stack
    mutating func pop() -> Element? {
        return elements.popLast()
    }
    
    // define a property to get the number of elements in the stack
    var count: Int {
        return elements.count
    }
    
    // define a property to check if the stack is empty
    var isEmpty: Bool {
        return elements.isEmpty
    }
}

In this example, we have defined a generic type called Stack that implements a stack data structure. We have used the syntax to indicate that the type has a generic type parameter Element. The Element type represents the type of the elements that the stack can store and retrieve. However, we don’t know what the Element type is exactly; we leave it as a placeholder that will be decided by the types of the elements when we create an instance of Stack.

We have then defined a private array to store the elements, and implemented the methods and properties required by a stack data structure. We have used the Element type as the type of the array elements and the method parameters and return types.

We can now use our Stack type with different types of data, such as Int and String. We can write something like this:

// create an instance of Stack with Int as Element
var numbers = Stack<Int>()

// use numbers as a stack of integers
numbers.push(1) // numbers is now [1]
numbers.push(2) // numbers is now [1, 2]
numbers.push(3) // numbers is now [1, 2, 3]
numbers.pop() // returns 3 and numbers is now [1, 2]
numbers.count // returns 2
numbers.isEmpty // returns false

// create an instance of Stack with String as Element
var words = Stack<String>()

// use words as a stack of strings
words.push("hello") // words is now ["hello"]
words.push("world") // words is now ["hello", "world"]
words.push("swift") // words is now ["hello", "world", "swift"]
words.pop() // returns "swift" and words is now ["hello", "world"]
words.count // returns 2
words.isEmpty // returns false

In this example, we have created two instances of Stack with different types of data: numbers and words. We have used them as stacks of integers and strings, respectively. We have used the same methods and properties defined by our Stack type to manipulate the stacks. This shows how generics allow us to create types that can work with any type of data.

How to use generic type constraints in Swift?

Generic constraints are rules that restrict the types of values that can be used in a generic function or type. By setting constraints, we can ensure that the generic code will only accept certain types of values, which helps to avoid runtime errors.

What are generic type constraints?

Generic constraints are rules that restrict the types of values that can be used in a generic function or type. By setting constraints, we can ensure that the generic code will only accept certain types of values, which helps to avoid runtime errors.

// This function takes two values of any type that conforms to Numeric and returns their sum
func addNumbers<T: Numeric>(_ a: T, _ b: T) -> T {
    return a + b
}
// This call passes two integers as arguments, which conform to Numeric, and returns their sum as an integer
let sum1 = addNumbers(10, 20) // returns 30

// This call passes two doubles as arguments, which conform to Numeric, and returns their sum as a double
let sum2 = addNumbers(4.5, 20.5) // returns 25.0

// This call tries to pass two strings as arguments, which do not conform to Numeric, and causes a compile-time error
let sum3 = addNumbers("2", "3") // Error: Cannot convert value of type 'String' to expected argument type 'Numeric'

Conclusion

Generics are a powerful feature of Swift that allow us to write code that can work with any type of data. By using them wisely, we can improve our code quality, flexibility, and abstraction. I hope this blog post has helped you to understand what are generics in Swift, how to use them in your code, and what are the benefits of using them.

Thanks for reading!