Type Erasure Pattern

We can use the type erasure pattern to combine both generic type parameters and associated types to satisfy both the compiler and our flexibility goals.

We will need to create three discrete types in order to satisfy these constraints. This pattern includes an abstract base class, a private box class, and finally a public wrapper class. This same pattern, as diagramed below, is even used in the Swift standard library.

Breaking Down Type Erasure in Swift_swift

Abstract Base

The first step of the process is to create an abstract base class. This base class will have three requirements:

  1. Conform to the ​​Row​​ protocol.
  2. Define a generic type parameter ​​Model​​ that serves as ​​Row​​’s associated type.
  3. Be abstract. (each function must be overridden by a subclass)

This class will not be used directly, but instead subclassed in order to bind the generic type constraint to our protocol’s associated type. We’ve marked this class ​​private​​ as well as by convention prefixing it with an ​​_​​.


 


Private Box

Next we create a ​​private​​ Box class with these requirements:

  1. Inherit from the generic base class ​​_AnyRowBase​​.
  2. Define a generic type parameter ​​Concrete​​ that itself conforms to ​​Row​​.
  3. Store an instance of ​​Concrete​​ for later usage.
  4. Trampoline each ​​Row​​ protocol function calls to the stored ​​Concrete​​ instance.

This class is referred to as a Box class, because it holds a reference to our concrete implementer of the protocol. In our case this would be a ​​FileCell​​, ​​FolderCell​​ or ​​DetailFileCell​​ all of which implement ​​Row​​. We receive the ​​Row​​ protocol conformance from our super class ​​_AnyRowBase​​ and override each function by trampolining the call over to the concrete class. Finally, this class serves a conduit to connect our concrete class’s associated type with our base classes generic type parameter.


 


Public Wrapper

With our private implementation details in place, we need to create a public interface for our type erased wrapper. The naming convention used for this pattern is to prefix ​​Any​​ in front of the protocol you’re wrapping, in our case ​​AnyRow​​.

This class has the following responsibilities:

  1. Conform to the ​​Row​​ protocol.
  2. Define a generic type parameter ​​Model​​ that serves as ​​Row​​s associated type.
  3. Within its initializer, take a concrete implementer of the ​​Row​​ protocol.
  4. Wrap the concrete implementer in a ​​private​​ Box ​​_AnyRowBase<Model>​​.
  5. Trampoline each ​​Row​​ function call along to the Box.

This final piece of the puzzle performs the actual type erasing. We supply the ​​AnyRow​​ class with a concrete implementer of ​​Row​​ (i.e. ​​FileCell​​) and it erases that concrete type allowing us to work with simply any adopter of ​​Row​​ with matching associated types (i.e. ​​AnyRow<File>​​).


 


Notice that while our concrete implementer is a property of type ​​_AnyRowBase<Model>​​, we have to wrap it up in an instance of ​​_AnyRowBox​​. The only requirement on ​​_AnyRowBox​​’s initializer is that the concrete class implements ​​Row​​. This is where the actual type easement occurs. Without this layer in our stack we’d be required to supply the associated type ​​Model​​ to ​​_AnyRowBase<Model>​​ explicitly; and we’d be back to square one.

The Payoff!

The biggest benefit to this process is all the messy boilerplate is an implementation detail. We are left with a relatively simple public API. Let’s revisit our original goals that are now possible with our type erased ​​AnyRow<Model>​​.


 


Homogeneous Requirement

One important thing to note is we haven’t lost all of the conveniences and safety of strong typing. With Swift type checker still helping us out we cannot hold an array of just any ​​AnyRow​​, our ​​Model​​ type must remain consistent.

This differs from how other more loosely typed languages such as Objective-C would handle the situation. The following is perfectly valid in Objective-C:


 


Swift, on the other hand, requires our array to be homogeneous around our protocol’s associated type. If we attempt the same line of code in ​​Swift​​ we’re presented with an error:


 


This is a good thing; we’re allowed enough flexibility to work with multiple types conforming to ​​Row​​ but we cannot be bitten by receiving a type we did not expect.

Wrapping Up

Assembling all the pieces required in the type erasure pattern can be overwhelming at first. Fortunately, it’s a formulaic process that will not differ based on your protocol. It’s such a systematic process that the Swift standard library may very well do this for you at some point. See the Swift Evolution’s Completing Generics Manifesto

To explore type erasure concepts in a Swift Playground, check out our accompanying Type Erasure Playgrounds:

  • Examples from this post
  • Type Erasure Excerpt from our Core Data Stack
  • Type Erasure with Pokemon

https://www.bignerdranch.com/blog/breaking-down-type-erasures-in-swift/


------------------越是喧嚣的世界,越需要宁静的思考------------------ 合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。 积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步;驽马十驾,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也。