Introduction
When working with protocols and generics in Swift, you may encounter situations where you need to hide the specific type information of an object and provide a more generic interface. This is where type erasure comes into play. In this blog post, we’ll dive deep into the concept of type erasure in Swift, explore its benefits, and walk through a practical code example to illustrate how it works.
If you are familar with Combine framework, you will see types like AnyPublisher
, or method like eraseToAnyPublisher
which wraps Publisher
types and return AnyPublisher
to hide the underlying type information. This is a pratical of the type earsure mechanism.
What is Type Erasure?
Type erasure is a powerful technique in Swift that allows you to conceal the specific type information of an object, presenting a more generic interface to the outside world. By erasing the specific type, you can work with objects in a more flexible and abstract manner, enhancing code reusability and modularity.
In Swift, type erasure is typically achieved using the Any
or AnyObject
types.
Any
can hold a reference to any typeAnyObject
can hold a reference to any class type.
By utilizing these types, you can erase the specific type information and interact with objects through a generic interface.
Another technique to erase types is to create a concrete wrapper type (such as AnyPublisher
), which hide away the details of the generic types.
Practical Example:
Let’s consider a practical example to demonstrate how type erasure works in Swift. Suppose we have an Animal
protocol that defines a name
property and a makeSound()
method:
protocol AnimalSound {
func play()
}
struct DefaultDogSound: AnimalSound {
func play() {
print("Woof!")
}
}
struct DefaultCatSound: AnimalSound {
func play() {
print("Meow!")
}
}
protocol Animal {
associatedtype Sound
var name: String { get }
func makeSound() -> Sound
}
We can create concrete implementations of the Animal
protocol, such as Dog
and Cat
classes:
class Dog: Animal {
var name: String
init(name: String) {
self.name = name
}
func makeSound() -> AnimalSound {
DefaultDogSound()
}
}
class Cat: Animal {
var name: String
init(name: String) {
self.name = name
}
func makeSound() -> AnimalSound {
DefaultCatSound()
}
}
Now, let’s say we want to work with a collection of animals without exposing their specific types. This is where type erasure comes in handy. We can create a type eraser struct called AnyAnimal
that conforms to the Animal
protocol:
struct AnyAnimal: Animal {
private let _name: () -> String
private let _makeSound: () -> AnimalSound
init<T: Animal>(_ animal: T) where T.Sound == AnimalSound {
_name = { animal.name }
_makeSound = { animal.makeSound() }
}
var name: String {
return _name()
}
func makeSound() -> AnimalSound {
_makeSound()
}
}
Inside AnyAnimal
, we have private properties _name
and _makeSound
of function types that capture the name
and makeSound()
implementations of the underlying animal. The init
method takes a generic parameter T
that conforms to the Animal
protocol and initializes the private properties with the corresponding implementations.
Using AnyAnimal
, we can create an array of animals without exposing their specific types:
let dog = Dog(name: "Buddy")
let cat = Cat(name: "Whiskers")
let animals: [AnyAnimal] = [AnyAnimal(dog), AnyAnimal(cat)]
We can then iterate over the animals
array and call the makeSound()
method on each animal, invoking the underlying implementation based on the specific animal type:
for animal in animals {
print("\(animal.name) says:")
let sound = animal.makeSound()
sound.play()
}
By using AnyAnimal
as the type eraser, we can work with different types of animals in a generic manner without exposing their specific types. The specific type information is erased, and we only interact with the Animal
protocol through the AnyAnimal
struct.
In the console, you will see the following as expected:
Buddy says:
Woof!
Whiskers says:
Meow!
Benefits of Type Erasure:
Type erasure offers several benefits in Swift programming:
-
Flexibility: Type erasure allows you to work with objects in a more flexible manner by hiding their specific types and providing a generic interface.
-
Abstraction: By erasing the specific types, you can achieve a higher level of abstraction in your code, making it more modular and reusable.
-
Protocol Conformance: Type erasure enables you to create collections or arrays of objects that conform to a specific protocol