In Swift, generic types are widely used as they provide flexibility and reuseability to our code. However, sometimes we may face a problem where we need to abstract a type that conforms to a certain protocol without disclosing its exact underlying type. In such cases, we can use a pattern called type erasure.
Type erasure is a design pattern that allows us to create a concrete type which hides the original concrete type while still conforming to a protocol.
To understand this pattern better, let's consider a simple example. Here, we have a protocol called Shape
which has a function for calculating its area:
protocol Shape {
func calculateArea() -> Double
}
We also have two concrete types, a Rectangle
and a Circle
, which conform to the Shape
protocol:
struct Rectangle: Shape {
let length: Double
let breadth: Double
func calculateArea() -> Double {
return length * breadth
}
}
struct Circle: Shape {
let radius: Double
func calculateArea() -> Double {
return Double.pi * radius * radius
}
}
Now, let's say we want to create an array of Shape
objects and calculate the total area of all shapes in the array:
let shapes: [Shape] = [Rectangle(length: 10, breadth: 5), Circle(radius: 5)]
var totalArea: Double = 0
for shape in shapes {
totalArea += shape.calculateArea()
}
print(totalArea) // Output: 223.4131591025766}
So far, everything works fine. However, what if we want to create a new function which takes an array of Shape
objects as a parameter and logs the area of each shape in the array:
func logAreaOfShapes(_ shapes: [Shape]) {
for shape in shapes {
print("Area of shape is \(shape.calculateArea())")
}
}
logAreaOfShapes(shapes)}
Here, we encounter a problem. The function takes an array of Shape
objects, but we cannot access the properties of the underlying concrete types, such as length
and breadth
for a Rectangle
or radius
for a Circle
.
To solve this problem, we can create a type-erased wrapper which conforms to the Shape
protocol. This wrapper can store the underlying concrete type and pass through the calculateArea()
function to the original concrete type:
struct AnyShape: Shape {
private let calculateAreaFunc: () -> Double
init(_ shape: T) {
calculateAreaFunc = shape.calculateArea
}
func calculateArea() -> Double {
return calculateAreaFunc()
}
}
Here, we have a new type called AnyShape
which takes a generic type parameter and conforms to the Shape
protocol. It stores the underlying concrete type's calculateArea()
function in a closure which is then passed through by the AnyShape
's own calculateArea()
function.
With this wrapper, we can now modify the logAreaOfShapes()
function to use the AnyShape
type instead:
func logAreaOfShapes(_ shapes: [AnyShape]) {
for shape in shapes {
print("Area of shape is \(shape.calculateArea())")
}
}
Now, we can convert our original Rectangle
and Circle
objects to AnyShape
objects and pass them to the logAreaOfShapes()
function:
let anyShapes: [AnyShape] = [AnyShape(Rectangle(length: 10, breadth: 5)), AnyShape(Circle(radius: 5))]
logAreaOfShapes(anyShapes)}
This will output the area of each shape:
Area of shape is 50.0
Area of shape is 78.53981633974483}
Summary: Type erasure is a design pattern that allows us to create a concrete type which hides the original concrete type while still conforming to a protocol. With the use of an AnyShape
type, we were able to abstract our concrete types and pass them as generic parameter to a function.