Working with Generics
1. Creating Generic Functions
Generic functions allow you to write flexible, reusable functions that work with any type.
| Function | Description | Example |
|---|---|---|
| func swapValues<T>(first: inout T, second: inout T) | Swaps the values of two variables of the same type | var a = 10<br>var b = 20<br>swapValues(first: "a, second: "b)<br>print(a) // 20<br>print(b) // 10 |
| func identity<T>(value: T) -> T | Returns the input value unchanged, useful for type inference | let str = identity(value: "Hello")<br>let num = identity(value: 42) |
2. Creating Generic Types
Generic types allow you to write flexible, reusable functions and classes that work with any type. They provide a way to create types that can be parameterized by other types.
| Feature | Description | Example |
|---|---|---|
| Generic Type Parameters | Define placeholders for types used in the generic type. | func swapTwoValues<T>(_ a: inout T, _ b: inout T) |
| Generic Classes | Classes that can work with any type specified at instantiation. | class Stack<Element> { ... } |
| Generic Functions | Functions that can operate on values of any type. | func identity<T>(value: T) -> T { return value } |
Example: Generic Stack Implementation
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
return items.popLast()
}
var isEmpty: Bool {
return items.isEmpty
}
}
var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
Example: Generic Function with Constraints
func allItemsEqual<T: Equatable>(array: [T]) -> Bool {
guard let first = array.first else { return true }
return array.allSatisfy { $0 == first }
}
let numbers = [1, 2, 3, 4]
let strings = ["hello", "world"]
print(allItemsEqual(array: numbers)) // false
print(allItemsEqual(array: strings)) // false
3. Using Type Constraints
| Constraint | Description | Example |
|---|---|---|
| : Equatable | Requires the type to conform to the Equatable protocol, allowing comparison with == and !=. |
|
| : Comparable | Requires the type to conform to Comparable, enabling ordering operations like <, >. |
|
| : Hashable | Requires the type to conform to Hashable, allowing use in collections like Set or as dictionary keys. |
|
| : AnyObject | Constrains the generic type to reference types (classes only), not value types like structs or enums. | |
| : Init | Ensures that the generic type has an initializer with a specific signature (e.g., init() or init?(String)). |
|
Example: Using Equatable constraint
func areEqual<T: Equatable>(first: T, second: T) -> Bool {
return first == second
}
let numbers = [1, 2, 3]
let strings = ["a", "b", "c"]
print(areEqual(first: numbers[0], second: numbers[1])) // false
print(areEqual(first: strings[0], second: strings[0])) // true
4. Using Associated Types in Protocols
Associated types in Swift protocols allow you to define a placeholder for a type that will be determined when the protocol is adopted. This enables more flexible and reusable code.
| Feature | Description | Example |
|---|---|---|
| Protocol with Associated Type | Defines a placeholder type using associatedtype |
|
| Adopting Protocol with Associated Type | Implement the protocol by specifying the concrete type | |
| Type Inference | Swift can infer the associated type from context | |
Example: Generic Protocol with Associated Types
protocol StackProtocol {
associatedtype Element
mutating func push(_ element: Element)
mutating func pop() -> Element?
var isEmpty: Bool { get }
}
struct IntStack: StackProtocol {
private var elements: [Int] = []
mutating func push(_ element: Int) {
elements.append(element)
}
mutating func pop() -> Int? {
return elements.popLast()
}
var isEmpty: Bool {
elements.isEmpty
}
}
var stack = IntStack()
stack.push(10)
stack.push(20)
print(stack.pop()) // Optional(20)
associatedtype keyword is used to define a placeholder for a type that will be determined when the protocol is adopted. This allows protocols to work with different types while maintaining type safety.5. Using Generic Where Clauses
The where clause in Swift generics allows you to specify constraints on generic types, making your code more expressive and type-safe. It's particularly useful for ensuring that a generic type conforms to a specific protocol or satisfies certain conditions.
| Constraint | Description | Example |
|---|---|---|
| Protocol Conformance | Ensures that a generic type conforms to a specific protocol. | func process<T: Codable>(item: T) { ... } |
| Associated Types | Allows constraints on associated types within a generic context. | func process<T>(item: T) where T: Collection, T.Element: Equatable { ... } |
| Type Equivalence | Ensures that two generic types are of the same type. | func compare<T>(a: T, b: T) where T == String { ... } |
Example: Protocol Conformance with Where Clause
protocol Drawable {
func draw()
}
struct Circle: Drawable {
func draw() { print("Drawing a circle") }
}
struct Square: Drawable {
func draw() { print("Drawing a square") }
}
func render<T>(shape: T) where T: Drawable {
shape.draw()
}
let circle = Circle()
let square = Square()
render(shape: circle) // Output: Drawing a circle
render(shape: square) // Output: Drawing a square
Example: Collection with Element Constraints
func processItems<T>(items: T) where T: Collection, T.Element: Equatable {
for item in items {
print(item)
}
}
let numbers = [1, 2, 3, 4, 5]
let strings = ["a", "b", "c"]
processItems(items: numbers)
processItems(items: strings)
where clause can also be used in type aliases and extensions to provide more specific constraints on generic types.
6. Creating Generic Subscripts
| Feature | Description | Example |
|---|---|---|
| Generic Subscript Declaration | Defines a subscript with generic type parameters. | subscript<T>(index: Int) -> T |
| Generic Type Parameter | Allows the subscript to work with different types. | subscript<T>(key: String) -> T |
Example: Generic Subscript in a Dictionary
struct GenericDictionary<T> {
private var data: [String: T] = [:]
subscript(key: String) -> T? {
return data[key]
}
subscript<U>(key: String) -> U? where U: Codable {
return data[key] as? U
}
}
let dict = GenericDictionary<String>()
dict["name"] = "John"
print(dict["name"]) // Optional("John")
7. Understanding Type Erasure
| Concept | Description | Example |
|---|---|---|
| Type Erasure | Process of hiding the concrete type of a generic, allowing for uniform handling of different types. | |
Example: Using Any and Type Erasure
protocol Drawable { func draw() }
struct Circle: Drawable { func draw() { print("Drawing circle") } }
struct Square: Drawable { func draw() { print("Drawing square") } }
// Type erasure using Any
func render(_ drawable: any Drawable) { drawable.draw() }
let circle = Circle()
let square = Square()
render(circle)
render(square)
Key Points
- Type erasure allows working with types that conform to a protocol without knowing their concrete type.
- Use
anykeyword to indicate a type that conforms to a protocol but hides its specific type. - Helpful for collections of different types that share a common protocol.
8. Using Opaque Types
Opaque types in Swift allow you to hide the concrete type of a value while still providing a strongly-typed interface. They are useful for encapsulating implementation details and returning values with a specific type without exposing the underlying type.
| Feature | Description | Example |
|---|---|---|
| Opaque Type Syntax | Use some keyword to declare an opaque return type. |
|
| Type Erasure | Allows hiding the concrete type while maintaining type safety. | |
| Protocol Conformance | Opaque types must conform to a specific protocol or set of protocols. | |
Example: Returning Opaque Types
protocol Shape {
func area() -> Double
}
struct Circle: Shape {
let radius: Double
func area() -> Double {
return Double.pi * radius * radius
}
}
struct Rectangle: Shape {
let width: Double
let height: Double
func area() -> Double {
return width * height
}
}
func makeShape(type: String) -> some Shape {
if type == "circle" {
return Circle(radius: 5.0)
} else {
return Rectangle(width: 4.0, height: 6.0)
}
}
some keyword is used to declare an opaque return type. It tells the compiler that the function returns a value of some specific type that conforms to the given protocol, but it doesn't reveal the actual type.
9. Understanding Generic Specialization
Generic specialization is the process by which the Swift compiler replaces generic type parameters with concrete types to create a specialized version of a generic type or function.
| Concept | Description | Example |
|---|---|---|
| Generic Type Parameter | A placeholder for a type that is determined when the generic is used | func process<T>(value: T) |
| Type Argument | The actual type used to specialize a generic | process(value: 42) - T becomes Int |
| Specialized Type | The concrete type created when a generic is specialized | process(value: 42) -> process<Int>(value: Int) |
Example: Generic Function Specialization
func identity<T>(value: T) -> T {
return value
}
let intResult = identity(value: 10) // Specialized as identity<Int>
let stringResult = identity(value: "Hello") // Specialized as identity<String>
Key Points of Generic Specialization
- Each unique type argument creates a new specialized version of the generic
- Specialization happens at compile time, not runtime
- Swift uses monomorphization to generate optimized code for each specialization
10. Working with Collection Generics
| Collection Type | Description | Generic Parameter | Example |
|---|---|---|---|
| Array<Element> | Ordered, mutable collection of elements | Element |
let numbers: Array<Int> = [1, 2, 3] |
| Set<Element> | Unordered, unique collection of elements | Element |
let uniqueNumbers: Set<Int> = [1, 2, 3] |
| Dictionary<Key, Value> | Key-value pairs with unique keys | Key, Value |
let ages: Dictionary<String, Int> = ["Alice": 30] |
Example: Using Generic Collections
let fruits: Array<String> = ["apple", "banana"]
let numbers: Set<Int> = [1, 2, 3, 4]
let person: Dictionary<String, Any> = ["name": "John", "age": 30]