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) {
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:
- Sizes of variables of type T;
- The addresses of the specific overload of function <, which must be called at run time.
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