The power of generics in Swift. Part 1

Hello! We are sharing with you a translation prepared especially for students of the course “iOS Developer. Advanced Course . Enjoy reading.







Generic function, generic type and type restrictions



What are generics?



When they work, you love them, and when not, you hate them!



In real life, everyone knows the power of generics: waking up in the morning, deciding what to drink, filling a cup.



Swift is a type-safe language. Whenever we work with types, we need to explicitly specify them. For example, we need a function that will work with more than one type. Swift has types Any



and AnyObject



, but they should be used carefully and not always. Using Any



and AnyObject



will make your code unreliable, because it will be impossible to track type mismatch during compilation. This is where generics come to the rescue.



Generic code allows you to create reusable functions and data types that can work with any type that meets certain restrictions, while ensuring type safety during compilation. This approach allows you to write code that helps to avoid duplication and expresses its functionality in a clear abstract manner. For example, types such as Array



, Set



and Dictionary



use generics to store elements.



Say we need to create an array consisting of integer values ​​and strings. To solve this problem, I will create two functions.



 let intArray = [1, 2, 3, 4] let stringArray = [a, b, c, d] func printInts(array: [Int]) { print(intArray.map { $0 }) } func printStrings(array: [String]) { print(stringArray.map { $0 }) }
      
      





Now I need to output an array of elements of type float or an array of user objects. If we look at the functions above, we will see that only the difference in type is used. Therefore, instead of duplicating the code, we can write a generic function for reuse.



The history of generics in Swift







Generic Functions



Generic function can work with any universal parameter of type T



The type name says nothing about what



should be, but it says that both arrays must be of type



, regardless of what



is. The type itself to use instead of



is determined each time the print(



_:



)







function is called.



 func print<T>(array: [T]) { print(array.map { $0 }) }
      
      





Generic types or parametric polymorphism



The generic type T from the example above is a type parameter. You can specify several type parameters by writing several type parameter names in angle brackets, separated by commas.



If you look at Array and Dictionary <Key, Element>, you can see that they have named type parameters, that is, Element and Key, Element, which speaks of the relationship between the type parameter and the generic type or function in which it is used .



Note: Always give names to type parameters in a CamelCase notation (for example, T



and TypeParameter



) to show that they are a name for the type and not a value.



Generic Types



These are custom classes, structures, and enumerations that can work with any type, similar to arrays and dictionaries.



Let's create a stack



 import Foundation enum StackError: Error { case Empty(message: String) } public struct Stack { var array: [Int] = [] init(capacity: Int) { array.reserveCapacity(capacity) } public mutating func push(element: Int) { array.append(element) } public mutating func pop() -> Int? { return array.popLast() } public func peek() throws -> Int { guard !isEmpty(), let lastElement = array.last else { throw StackError.Empty(message: "Array is empty") } return lastElement } func isEmpty() -> Bool { return array.isEmpty } } extension Stack: CustomStringConvertible { public var description: String { let elements = array.map{ "\($0)" }.joined(separator: "\n") return elements } } var stack = Stack(capacity: 10) stack.push(element: 1) stack.push(element: 2) print(stack) stack.pop() stack.pop() stack.push(element: 5) stack.push(element: 3) stack.push(element: 4) print(stack)
      
      





Now this stack is able to accept only integer elements, and if I need to store elements of a different type, I will either need to create another stack, or convert this to a generic look.



 enum StackError: Error { case Empty(message: String) } public struct Stack<T> { var array: [T] = [] init(capacity: Int) { array.reserveCapacity(capacity) } public mutating func push(element: T) { array.append(element) } public mutating func pop() -> T? { return array.popLast() } public func peek() throws -> T { guard !isEmpty(), let lastElement = array.last else { throw StackError.Empty(message: "Array is empty") } return lastElement } func isEmpty() -> Bool { return array.isEmpty } } extension Stack: CustomStringConvertible { public var description: String { let elements = array.map{ "\($0)" }.joined(separator: "\n") return elements } } var stack = Stack<Int>(capacity: 10) stack.push(element: 1) stack.push(element: 2) print(stack) var strigStack = Stack<String>(capacity: 10) strigStack.push(element: "aaina") print(strigStack)
      
      





Generic Type Limitations



Since a generic can be of any type, you can’t do much with it. It is sometimes useful to apply constraints to types that can be used with generic functions or generic types. Type restrictions indicate that the type parameter must match a specific protocol or protocol composition.



For example, the Swift Dictionary



type imposes restrictions on types that can be used as keys for a dictionary. The dictionary requires that the keys be hashed in order to be able to check whether it already contains values ​​for a particular key.



 func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here }
      
      





Essentially, we created a stack of type T, but we cannot compare two stacks, because the types here do not match Equatable



. We need to change this to use Stack<



T:



Equatable



>







.



How do generics work? Let's look at an example.



 func min<T: Comparable>(_ x: T, _ y: T) -> T { return y < x ? y : x }
      
      





The compiler lacks two things necessary to create the function code:





Whenever the compiler encounters a value that is of type generic, it places the value in a container. This container has a fixed size for storing values. In case the value is too large, Swift allocates it on the heap and stores a link to it in the container.



The compiler also maintains a list of one or more witness tables for each generic parameter: one witness table for values, plus one witness table for each type restriction protocol. Witness tables are used to dynamically send function calls to desired implementations at runtime.



The end of the first part. By tradition, we are waiting for your comments, friends.



The second part of



All Articles