Breaking Down Type Erasure in Swift
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.
Abstract Base
The first step of the process is to create an abstract base class. This base class will have three requirements:
- Conform to the
Row
protocol. - Define a generic type parameter
Model
that serves asRow
’s associated type. - 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:
- Inherit from the generic base class
_AnyRowBase
. - Define a generic type parameter
Concrete
that itself conforms toRow
. - Store an instance of
Concrete
for later usage. - Trampoline each
Row
protocol function calls to the storedConcrete
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:
- Conform to the
Row
protocol. - Define a generic type parameter
Model
that serves asRow
s associated type. - Within its initializer, take a concrete implementer of the
Row
protocol. - Wrap the concrete implementer in a
private
Box_AnyRowBase<Model>
. - 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:
https://www.bignerdranch.com/blog/breaking-down-type-erasures-in-swift/
相关文章
- How to handle uncaught exception in Blazor project globally?
- openxml in sql server
- 解决main.o(.data) type RW incompatible with bsp.o(.ARM.__AT_0x24001000) type ZI in er RW_IRAM2.(转载)
- Python的for语句for i in range(N)到底是执行多少次循环
- Implementing a Statistical Anomaly Detector in Elasticsearch - Part 1
- 【JS】for in循环对象,hasOwnProperty()的作用
- pip报错:Fatal error in launcher: Unable to create process using ‘“‘
- java.lang.NoSuchFieldError: No static field abc_ic_ab_back_mtrl_am_alpha of type I in class Landroid/support/v7/appcompat/R$drawable
- A Deep Dive into Rescalable State in Apache Flink
- There is no type initializer in Swift----One answer is to use static, it is the same as class final.
- A “Type Erasure” Pattern that Works in Swift:类型域的转换
- In PyTorch 1.1.0 and later, you should call them in the opposite order: `optimizer.step()` before `l
- mybatis方法参数报错:Parameter 'xxx' not found - @requestParam 和@param的区别、desc关键字作为了字段名称查询问题、id取值错乱问题、ERROR:syntax error at or near "$8" in postgres
- [LeetCode] Find All Numbers Disappeared in an Array 找出数组中所有消失的数字
- install docker in manjaro
- oracle 使用 in 关键字查询且集合数量大小大于1000