Swift - Generics

naveenragul

Naveen Ragul B

Posted on April 6, 2022

Swift - Generics

Generic code enables you to write flexible, reusable(avoids duplication) functions and types that can work with any type, subject to requirements that you define.

  • Array, Set and Dictionary are Generic types in Swift.

Generic Functions

Generic functions can work with any type. The generic version of the function uses a placeholder type name instead of an actual type name. The actual type to use in place of placeholder is determined each time when generic function is called.

  • Type parameters specify and name a placeholder type, and are written immediately after the function’s name, between a pair of matching angle brackets (such as <T>). Multiple type parameters separated by comma can be specified.

example :

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}
Enter fullscreen mode Exit fullscreen mode

Generic Types

These are custom classes, structures, and enumerations that can work with any type.

example :

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

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
Enter fullscreen mode Exit fullscreen mode
  • 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.

example :

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Extensions of a generic type can also include requirements that instances of the extended type must satisfy in order to gain the new functionality using generic where clause.

Type Constraints

Type constraints specify that a type parameter must inherit from a specific class, or conform to a particular protocol or protocol composition.

synatx :

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

Here parameters have type constraints :

  1. T to be a subclass of SomeClass
  2. U to conform to the protocol SomeProtocol

example :

func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

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.

  • Associated types are specified with the associatedtype keyword.

example :

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
Enter fullscreen mode Exit fullscreen mode
struct Stack<Element>: Container {
    // original Stack<Element> implementation
    var items: [Element] = []
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // conformance to the Container protocol
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Existing type can be extended to add conformance to a protocol.

example :

extension Array: Container {}
Enter fullscreen mode Exit fullscreen mode
  • type constraints can be added to an associated type in a protocol to require that conforming types satisfy those constraints. example :
protocol Container {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
Enter fullscreen mode Exit fullscreen mode
  • A protocol can appear as part of its own requirements. example :
protocol SuffixableContainer: Container {
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}
Enter fullscreen mode Exit fullscreen mode

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.

example :

extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
Enter fullscreen mode Exit fullscreen mode

Generic Where Clauses

Type constraints used to define requirements on the type parameters associated with a generic function, subscript, or type.

To define requirements for associated types, define a generic where clause. A generic where clause enables you to require that

  1. an associated type must conform to a certain protocol, or
  2. certain type parameters and associated types must be the same.

example :

func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

        // Check that both containers contain the same number of items.
        if someContainer.count != anotherContainer.count {
            return false
        }

        // Check each pair of items to see if they're equivalent.
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // All items match, so return true.
        return true
}
Enter fullscreen mode Exit fullscreen mode
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")

var arrayOfStrings = ["uno", "dos", "tres"]

if allItemsMatch(stackOfStrings, arrayOfStrings) {
    print("All items match.")
} else {
    print("Not all items match.")
} // Prints "All items match."
Enter fullscreen mode Exit fullscreen mode

Here even though the stack and the array are of a different type, they both conform to the Container protocol, and both contain the same type of values.

  • generic where clause as part of an extension.

example :

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}
Enter fullscreen mode Exit fullscreen mode

Contextual Where Clauses

generic where clause can be included as part of a declaration that doesn’t have its own generic type constraints, when you’re already working in the context of generic types.

extension Container {
    func average() -> Double where Item == Int {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }
    func endsWith(_ item: Item) -> Bool where Item: Equatable {
        return count >= 1 && self[count-1] == item
    }
}
Enter fullscreen mode Exit fullscreen mode

is same as

extension Container where Item == Int {
    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }
}
extension Container where Item: Equatable {
    func endsWith(_ item: Item) -> Bool {
        return count >= 1 && self[count-1] == item
    }
}
Enter fullscreen mode Exit fullscreen mode

Associated Types with a Generic Where Clause

generic where clause can be included on an associated type.

example :

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }

    associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
}
Enter fullscreen mode Exit fullscreen mode

Generic Subscripts

Subscripts can be generic, and they can include generic where clauses.
example :

extension Container {
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Iterator.Element == Int {
            var result: [Item] = []
            for index in indices {
                result.append(self[index])
            }
            return result
    }
}
Enter fullscreen mode Exit fullscreen mode

The generic where clause requires that the iterator for the sequence must traverse over elements of type Int. This ensures that the indices in the sequence are the same type as the indices used for a container.

💖 💪 🙅 🚩
naveenragul
Naveen Ragul B

Posted on April 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related