Generics
Generics allow you to write flexible, reusable code that works with multiple types while defining specific requirements for those types. This approach reduces duplication and clearly expresses your code's intent in an abstract way.
Generics power much of Swift's standard library. For instance, types like Array and Dictionary are generic, letting you store integers, strings, or any other type without restrictions on the type itself.
Why Use Generics?
Consider a simple function to swap two integer values:
func swapTwoNumbers(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}This function uses in-out parameters to exchange values. You can call it like this:
var num1 = 5
var num2 = 42
swapTwoNumbers(&num1, &num2)
print("num1 is now \(num1), num2 is now \(num2)")
// Prints "num1 is now 42, num2 is now 5"It's handy but limited to integers. To swap strings or doubles, you'd need separate functions:
func swapTwoTexts(_ a: inout String, _ b: inout String) {
let temp = a
a = b
b = temp
}
func swapTwoFloats(_ a: inout Double, _ b: inout Double) {
let temp = a
a = b
b = temp
}These functions are identical except for the types. A generic version would be more efficient, handling any type as long as the values match.
Note: The types of
aandbmust be identical for swapping to work. Swift's type safety prevents mismatched swaps, causing compile-time errors.
Generic Functions
Generic functions adapt to any type. Here's a generic swap function:
func swapAny<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}Compare the signatures:
func swapTwoNumbers(_ a: inout Int, _ b: inout Int)
func swapAny<T>(_ a: inout T, _ b: inout T)The generic version uses T as a placeholder. It requires a and b to share the same type T, inferred at call time.
You can use it for various types:
var num1 = 5
var num2 = 42
swapAny(&num1, &num2)
// num1 is now 42, num2 is now 5
var text1 = "apple"
var text2 = "banana"
swapAny(&text1, &text2)
// text1 is now "banana", text2 is now "apple"Note: Swift's standard library includes a
swap(_:_:)function, so use that in production code.
Type Parameters
Type parameters like T act as placeholders in function definitions, parameters, returns, or bodies. They're replaced by actual types at runtime. Use multiple parameters separated by commas, e.g., <T, U>.
Naming Type Parameters
Use descriptive names like Key or Value for clarity (e.g., in Dictionary<Key, Value>). For unrelated types, use single letters like T, U. Always use upper camel case to denote types.
Generic Types
Define custom generic classes, structs, or enums. Here's a generic Queue struct for any type:
struct Queue<Element> {
var elements: [Element] = []
mutating func enqueue(_ element: Element) {
elements.append(element)
}
mutating func dequeue() -> Element {
return elements.removeFirst()
}
}Element is the placeholder. Create instances like Queue<String>():
var queueOfFruits = Queue<String>()
queueOfFruits.enqueue("apple")
queueOfFruits.enqueue("banana")
queueOfFruits.enqueue("cherry")
// Queue now has 3 fruitsDequeue removes the first item:
let firstFruit = queueOfFruits.dequeue()
// firstFruit is "apple", queue now has 2 fruitsExtending a Generic Type
Extensions inherit the original type parameters:
extension Queue {
var firstElement: Element? {
return elements.isEmpty ? nil : elements[0]
}
}Use firstElement without dequeuing:
if let first = queueOfFruits.firstElement {
print("First fruit is \(first).")
}
// Prints "First fruit is banana."Type Constraints
Restrict type parameters to subclasses or protocols. Swift's Dictionary requires hashable keys via the Hashable protocol.
Syntax:
func exampleFunc<T: BaseClass, U: Protocol>(t: T, u: U) {
// Body
}Example: A function to find an item's index, requiring equatability:
func locateIndex<T: Equatable>(of target: T, in list: [T]) -> Int? {
for (idx, val) in list.enumerated() {
if val == target {
return idx
}
}
return nil
}Use it:
let animals = ["elephant", "tiger", "lion"]
let idx = locateIndex(of: "tiger", in: animals)
// idx is 1Without Equatable, equality checks fail to compile.
Associated Types
Protocols can declare associated types with associatedtype:
protocol Holder {
associatedtype Content
mutating func add(_ content: Content)
var size: Int { get }
subscript(i: Int) -> Content { get }
}Conformers specify the type:
struct IntQueue: Holder {
var elements: [Int] = []
mutating func add(_ content: Int) {
elements.append(content)
}
var size: Int { return elements.count }
subscript(i: Int) -> Int { return elements[i] }
typealias Content = Int // Optional due to inference
}Generic types infer similarly.
Extend existing types:
extension Array: Holder {}Add constraints:
protocol Holder {
associatedtype Content: Equatable
// ...
}Protocols can self-reference:
protocol SubHolder: Holder {
associatedtype Sub: SubHolder where Sub.Content == Content
func subSection(_ length: Int) -> Sub
}Generic Where Clauses
Add requirements before a body:
func matchAll<H1: Holder, H2: Holder>(_ one: H1, _ two: H2) -> Bool
where H1.Content == H2.Content, H1.Content: Equatable {
if one.size != two.size { return false }
for i in 0..<one.size {
if one[i] != two[i] { return false }
}
return true
}Extensions with a Generic Where Clause
extension Queue where Element: Equatable {
func isFirst(_ item: Element) -> Bool {
guard let first = elements.first else { return false }
return first == item
}
}Or for specific types:
extension Holder where Content == Float {
func mean() -> Float {
var total: Float = 0
for i in 0..<size { total += self[i] }
return total / Float(size)
}
}Contextual Where Clauses
Place where in declarations without full generics:
extension Holder {
func mean() -> Double where Content == Int {
var total = 0.0
for i in 0..<size { total += Double(self[i]) }
return total / Double(size)
}
func isLast(_ item: Content) -> Bool where Content: Equatable {
return size >= 1 && self[size-1] == item
}
}Equivalent to separate extensions.
Associated Types with a Generic Where Clause
protocol Holder {
// ...
associatedtype Traverser: IteratorProtocol where Traverser.Element == Content
func createTraverser() -> Traverser
}Inherit and constrain:
protocol SortedHolder: Holder where Content: Comparable { }Generic Subscripts
extension Holder {
subscript<Indexes: Sequence>(indexes: Indexes) -> [Content]
where Indexes.Iterator.Element == Int {
var res: [Content] = []
for idx in indexes { res.append(self[idx]) }
return res
}
}Implicit Constraints
Generic code often implies conformance to protocols like Copyable. Suppress with ~:
func process<Val: ~Copyable>(inout val: Val) {
// Cannot copy val here
}