Swift enables us to create generic types, protocols, and functions, that aren’t tied to any specific concrete type — but can instead be used with any type that meets a given set of requirements.
Being a language that strongly emphasises type safety, generics is an essential feature that’s core to many aspects of Swift — including its standard library, which uses generics quite heavily. Just look at some of its fundamental data structures, like Array and Dictionary, both of which are generics.
Generics enable the same type, protocol, or function, to be specialised for a large number of use cases. For example, since Array is a generic, it allows us to create specialised instances of it for any kind of type.
var array = ["One", "Two", "Three"]
array.append("Four")
// This won't compile, since the above array is specialised
// for strings, meaning that other values can't be inserted:
array.append(5)
// As we pull an element out of the array, we can still treat
// it like a normal string, since we have full type safety.
let characterCount = array[0].count
To create a generic of our own, we simply have to define what our generic types are, and optionally attach constraints to them. For example, here we’re creating a Container type that can contain any value, along with a date:
struct Container<Value> {
var value: Value
var date: Date
}
Just like how we’re able to create specialized arrays and dictionaries, we can specialize the above Container for any kind of value, such as strings or integers:
let stringContainer = Container(value: "Message", date: Date())
let intContainer = Container(value: 7, date: Date())
Note that we don’t need to specify what concrete types we’re specializing Container with above — Swift’s type inference automatically figures out that stringContainer is a Container instance, and that intContainer is an instance of Container.
Generic Types
These are custom classes, structures, and enumerations that can work with any type, in a similar way to Array and Dictionary.
Let’s create stack,
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 Constraints
Since a generic can be of any type, we can’t do a lot with it. It’s sometimes useful to enforce type constraints on the types that can be used with generic functions and generic types. Type constraints specify that a type parameter must conform to a particular protocol or protocol composition.
For example, Swift’s Dictionary type places a limitation on the types that can be used as keys for a dictionary. Dictionary needs its keys to be hashable so that it can check whether it already contains a value for a particular key.
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
Associated Types
An associated type gives a placeholder name to a type that’s used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted.
Adding Constraints to an Associated Type
You can add type constraints to an associated type in a protocol to require that conforming types satisfy those constraints.
protocol Stackable {
associatedtype Element: Equatable
mutating func push(element: Element)
mutating func pop() -> Element?
func peek() throws -> Element
func isEmpty() -> Bool
func count() -> Int
}
Now, Stack’s Element type needs to conform to Equatable else it will give compile time error.
Recursive Protocol Constraints:
A protocol can appear as part of its own requirements.
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
Suffix has two constraints: It must conform to the SuffixableContainer protocol (the protocol currently being defined), and its Item type must be the same as the container’s Item type.
Extending Generic Type:
When you extend a generic type, you don’t provide a type parameter list as part of the extension’s definition. Instead, the type parameter list from the original type definition is available within the body of the extension, and the original type parameter names are used to refer to the type parameters from the original definition.
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
Extensions with a Generic Where Clause
You can also use a generic where clause as part of an extension. The example below extends the generic Stack structure from the previous examples to add an isTop(_:)method.
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
The extension adds the isTop(_:)method only when the items in the stack are equatable. You can use a generic where clause with extensions to a protocol too. Multiple requirements can be added in where clause by separating with a comma.
Generic Specialization:
Generic specialization means that the compiler clones a generic type or function, such as Stack, for a concrete parameter type, such as Int. This specialized function can then be optimized specifically for Int, removing all indirection. The process of replacing type parameters with type arguments at compile time is known as specialization.
Top comments (0)