Swift Generics

Generics are used to write flexible and reusable code that can work across multiple types. So instead of creating a different function/struct for every type such as Int, String, etc, we can set a Generic type. A generic type is also known as a placeholder type. Typically when we pass typed parameters in a function we do something like

func printInt(aValue: Int) {
    print(aValue)
}

printInt(aValue: 1)


func printString(aString: String) {
    print(aString)
}

printString(aString: "balututorial.com")

Now we can easily use generic parameters instead of creating a different function for each type. For creating a generic function you need to set a placeholder value after the function name in angular brackets as <Element>.

You need to use the same placeholder value as parameters/return types. You can pass more than one placeholder values too. Typically, if the generic parameter placeholder doesn’t represent anything, use T, U, V, etc.

func printAnyThing<T>(a: T) {
    print(a)
}

printAnyThing(a: 1)

printAnyThing(a: "Balututorial.com")

Another example: The classical way of swapping values:

func swapData<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var x = "Balu"
var y = "Tutorial"
swapData(&x, &y)
print("x:\(x) y:\(y)")

var valueX = 2
var valueY = 3
swapData(&valueX, &valueY)
print(valueX)
print(valueY)
You can only pass the same type in all the T’s. Setting different types would cause a compiler error. We can tackle the above rule by using two generic parameters. We’ll have to declare both of them in the function header itself
func printTandU<T,U>(a:T,b:U) {
    print("T is \(a) and U is \(b)")
}

printTandU(a: 1, b: "Swift Generics")

Generic Type Constraints

We can constrain a Generic type to conform to a certain type also.

import Foundation

class Person {
    
    var name: String?
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

func printDetails<T: Person>(person1: T, person2: T) {
    print("Name: is \(person1.name ?? "nil") and age \(person1.age)")
    print("Name: is \(person2.name ?? "nil") and age \(person2.age)")
}

var p1 = Person(name: "Balu Naik",age: 31)
var p2 = Person(name: "Balututorial.com",age: 1)
printDetails(person1: p1, person2: p2)

T conforms to the type of Person. So you cannot pass any value that isn’t of the type Person in the above code. Doing the following would lead to a crash.

class AClass {
    
}
printDetails(person1: AClass(), person2: AClass()) //Error: Class AClass isn't of the type Person.

Subclasses would conform to the generic parameter type. Another example where the generic type must conform to the protocol

func compareAandB<T: Equatable>(a: T, b: T) {
    print("a == b ? \(a==b)")
}

compareAandB(a: 2, b: 3)
compareAandB(a: "Hai", b: "Hello")

Here the == won’t work without the Equatable protocol.

Using Extensions on Generic Types

We can use Extensions on a Generic type in the following way:

import Foundation

struct Stack<T> {
    
    var items = [T]()
    
    mutating func push(_ item: T) {
        items.append(item)
    }
    
    mutating func pop() -> T {
        return items.removeLast()
    }
}
var stackOfInt = Stack<Int>()
stackOfInt.push(2)
stackOfInt.push(3)

print(stackOfInt)  //Stack<Int>(items: [2, 3])

Using the where clause

We can use a where clause for an even stricter Generic type constraint checking. In the where clause we can add additional conditions.

import Foundation

protocol Hogwarts {}
protocol Muggles {}

class Person: Hogwarts {
    
    var name: String?
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class MClass: Person, Muggles {}

func printDetails<T:Person>(a: T) where T:Muggles {
    print("a is \(a.name ?? "NA") and age \(a.age)")
}

var p1 = Person(name: "Balu Naik",age: 31)
var p2 = Person(name: "Ron",age: 111)
var m2 = MClass(name: "Balututorial.com",age: 1)


//printDetails(a: p1)  //Error: Argument type 'Person' does not conform to expected type 'Muggles'
//printDetails(a: p2)  //Error: Argument type 'Person' does not conform to expected type 'Muggles'

printDetails(a: m2)

So in the above code, we add a checker wherein the type T must conform to Muggles protocol as well besides conforming to the class Person.

Hence it’ll take only types of class M in the above code. Not of the class Person.