The Power of Generics : Swift
One of the most powerful features introduced in Swift was Generics. Generics are used to avoid duplication and to provide abstraction.
The generic code allows you to write flexible, reusable functions and data types that can work with any type that matches the defined constraints.
Swift standard libraries are built with generics code. Swift’s ‘Array’ and ‘Dictionary’ types belong to generic collections.
We will learn below things in the article,
- Writing a Generic Data Structure
- Writing a Generic Function
- Extending a Generic Type
- Constraining a Generic Type
- Generics using the where clause
Writing a Generic Data Structure:
A queue is a data structure 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).
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, which can only enqueue and dequeue strings and integers, respectively.
struct Queue<Element> { fileprivate var elements: [Element] = [] mutating func enqueue(newElement: Element) { elements.append(newElement) } mutating func dequeue() -> Element? { guard !elements.isEmpty else { return nil } return elements.remove(at: 0) } } var q = Queue<Int>() q.enqueue(newElement: 10) print("Queue elements : \(q.elements)") //Output : "Queue elements : [10]" q.enqueue(newElement: 20) print("Queue elements : \(q.elements)") //Output : "Queue elements : [10, 20]" q.dequeue() print("Queue elements : \(q.elements)") //Output : "Queue elements : [20]" q.dequeue() print("Queue elements : \(q.elements)") //Output : "Queue elements : []"
Writing a Generic Function:
Typically when we pass typed parameters in a function we do something like,
func printMe(_ intVal: Int) { print(intVal) } printMe(1) //Output : 1 func printMe(_ stringVal: String) { print(stringVal) } printMe("Swift") //Output : Swift //Generic function func printMe<T>(_ val: T) { print(val) } printMe(1) //Output : 1 printMe("Swift") //Output : Swift
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.
Extending a Generic Type:
Extending the queue to know the top of the item is included with the ‘extension’ keyword.
//Generic queue declaration struct Queue<Element> { fileprivate var elements: [Element] = [] mutating func enqueue(newElement: Element) { elements.append(newElement) } mutating func dequeue() -> Element? { guard !elements.isEmpty else { return nil } return elements.remove(at: 0) } } var q = Queue<Int>() extension Queue { var first: Element? { return elements.isEmpty ? nil : elements[elements.count - 1] } } if let first = q.first { print("The top item on the Queue is \(first).") } //Output : nil q.enqueue(newElement: 30) if let first = q.first { print("The top item on the Queue is \(first).") } //Output : "The top item on the Queue is 30."
Constraining a Generic Type:
We can constrain a Generic type to conform to a certain type also.
class Person { var name:String? var age:Int init(name:String, age:Int) { self.name = name self.age = age } } func printDetails<T: Person>(a: T, b: T) { print("a is \(a.name ?? "No Name") and age \(a.age)") print("\n" + "b is \(b.name ?? "No Name") and age \(b.age)") } var p1 = Person(name: "Tom",age: 10) var p2 = Person(name: "Jerry",age: 20) printDetails(a: p1, b: p2) //Output : "a is Tom and age 10" // "b is Jerry and age 20" T conforms to the type Person. So you cannot pass any value that isn’t of the type Person in the above code. Another example where the generic type must conform to the protocol, func compareAandB<T: Equatable>(a: T, b: T) { print("a and b is \((a != b) ? "not" : "") equal.") } compareAandB(a: 1, b: 1) //Output : a and b is equal. compareAandB(a: 1, b: 2) //Output : a and b is not equal. compareAandB(a: "OK", b: "OK") //Output : a and b is equal. compareAandB(a: "OK", b: "Not OK") //Output : a and b is not equal.
Here the != won’t work without the Equatable protocol conformance.
Generics 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.
protocol General{} protocol Military{} class Person : General { var name:String? var age:Int init(name:String, age:Int) { self.name = name self.age = age } } class AnotherPerson: Person, Military {} func printDetails<T:Person>(a: T) where T:Military { print("Person is \(a.name ?? "No Name") and age is \(a.age)") } var objP1 = Person(name: "Jane",age: 71) var objAP2 = AnotherPerson(name: "Roger",age: 37) var objP2 = Person(name: "Dev",age: 19) printDetails(a: objP1) //Output : Compiletime Error ~> Generic parameter 'T' could not be inferred printDetails(a: objP2) //Output : Compiletime Error ~> Generic parameter 'T' could not be inferred printDetails(a: objAP2) //Output : Person is Roger and age is 37
So in the above code, we add a checker wherein the type T must conform to Military protocol as well besides conforming to the class Person.
Hence it’ll take only types of class AnotherPerson in the above code.
Swift generics are at the core of many common language features, such as arrays and optional. You’ve seen how to use them to build elegant, reusable code.
Generics in Swift is an integral feature that you’ll use every day to write powerful and type-safe abstractions.