Preface
In the Swift world, if we call the agreement a king, then generics can be regarded as queens. As the saying goes, one mountain does not allow two tigers. When we use these two together, we seem to encounter great difficulties. So is there a way to combine these two concepts so that they can be stepping stones on our way forward, not in the way? The answer is yes, here we will use the powerful feature of Type Erasure.
You may have heard of type erasing, and even used type erasing types like AnySequence provided by the standard library. But exactly what is type erase? How to customize type erase? In this post, I will discuss how to use type erase and how to customize it. Thanks to Lorenzo Boaro for this topic.
Sometimes you want to hide the specific type of a class from an external caller, or some implementation details. In some cases, doing so prevents static types from being abused in the project or guarantees interaction between types. Type erasing is the process of removing a specific type of a class to make it more general.
A protocol or abstract parent class can be used as one of the simple implementations of type erasing. For example, NSString is an example. Every time an NSString instance is created, this object is not an ordinary NSString object. It is usually an instance of a specific subclass. This subclass is generally private, and these details are usually hidden. You can use the functionality provided by subclasses without knowing its specific type, and you don't have to associate your code with their specific types.
When dealing with Swift generics and associated type protocols, you may need to use some advanced content. Swift does not allow the use of protocols as specific types. For example, if you want to write a method whose parameters are a sequence containing Int, then the following approach is incorrect:
func f(seq: Sequence<Int>) { ...
You cannot use the protocol type like this, as this will cause an error during compilation. But you can use generics instead of protocols to solve this problem:
func f<S: Sequence>(seq: S) where == Int { ...
Sometimes it's perfectly OK to write this way, but there are some troublesome situations in some places. Usually you can't just add generics in one place: a generic function requires more for other generics... What's worse, you can't use generics as return values or attributes. This is a little different from what we think.
func g<S: Sequence>() -> S where == Int { ...
We want the function g to return any matching type, but the above is different, it allows the caller to select the type he needs, and then the function g provides a suitable value.
AnySequence is provided in the Swift standard library to help us solve this problem. AnySequence wraps a sequence of any type and erases its type. Use AnySequence to access this sequence, let's override the function f and function g:
func f(seq: AnySequence<Int>) { ... func g() -> AnySequence<Int> { ...
The generic part is gone, and the specific types are also hidden. Because of the use of AnySequence to wrap specific values, it brings some code complexity and runtime costs. But the code is more concise.
Many of these types are provided in the Swift standard library, such as AnyCollection, AnyHashable and AnyIndex. These types work very well when you customize generics or protocols, and you can also use these types directly to simplify your code. Next, let's explore various ways to implement type erasing.
Class-based type erasing
Sometimes we need to wrap some common functionality from multiple types without exposing type information, which sounds like a parent-subclass relationship. In fact, we can indeed use abstract parent classes to implement type erasing. The parent class provides an API interface, and it doesn't matter who implements it. Subclasses realize corresponding functions based on specific type information.
Next we will customize AnySequence in this way, and we name it MAnySequence:
class MAnySequence<Element>: Sequence {
This class requires an iterator type as the makeIterator return type. We have to do two type erases to hide the underlying sequence type and the iterator type. We define an Iterator class inside MAnySequence, which follows the IteratorProtocol protocol and throws an exception using fatalError in the next() method. Swift itself does not support abstract types, but that's enough:
class Iterator: IteratorProtocol { func next() -> Element? { fatalError("Must override next()") } }
MAnySequence implements the makeIterator method similarly. A direct call will throw an exception, which is used to prompt the subclass to rewrite this method:
func makeIterator() -> Iterator { fatalError("Must override makeIterator()") } }
This defines a class-based type erasing API, and private subclasses implement these APIs in the future. Public classes are parameterized by element types, but private implementation classes are parameterized by the sequence types it wraps:
private class MAnySequenceImpl<Seq: Sequence>: MAnySequence<> {
MAnySequenceImpl requires a subclass inherited from Iterator:
class IteratorImpl: Iterator {
IteratorImpl wraps the iterator of the sequence:
var wrapped: init(_ wrapped: ) { = wrapped }
Call the wrapped sequence iterator in the next method:
override func next() -> ? { return () } }
Similarly, MAnySequenceImpl wraps a sequence:
var seq: Seq init(_ seq: Seq) { = seq }
Get the iterator from the sequence and wrap the iterator into an IteratorImpl object to return, thus implementing the makeIterator function.
override func makeIterator() -> IteratorImpl { return IteratorImpl(()) } }
We need a way to actually create these things: add a static method to MAnySequence which creates a MAnySequenceImpl instance and returns it to the caller as a MAnySequence type.
extension MAnySequence { static func make<Seq: Sequence>(_ seq: Seq) -> MAnySequence<Element> where == Element { return MAnySequenceImpl<Seq>(seq) } }
In actual development, we may do some extra operations to let MAnySequence provide an initialization method.
Let's try MAnySequence:
func printInts(_ seq: MAnySequence<Int>) { for elt in seq { print(elt) } } let array = [1, 2, 3, 4, 5] printInts((array)) printInts((array[1 ..< 4]))
Perfect!
Function-based type erasing
Sometimes we want to expose methods that support multiple types of methods, but we don’t want to specify specific types. An easy way is to store those signatures that only involve the types we want to expose, and the function body is created in the context of the underlying known concrete implementation type.
Let's take a look at how to use this method to design MAnySequence, which is very similar to the previous implementation. It is a structure rather than a class, because it is used only as a container and does not require any inheritance relationship.
struct MAnySequence<Element>: Sequence {
As before, MAnySequence also requires a returnable iterator (Iterator). The iterator is also designed as a structure and holds a stored property with empty parameters and returns Element?. In fact, this property is a function that is used in the next method of the IteratorProtocol protocol. Next Iterator follows the IteratorProtocol protocol and calls the function in the next method:
struct Iterator: IteratorProtocol { let _next: () -> Element? func next() -> Element? { return _next() } }
MAnySequence is very similar to Iterator: it holds a stored property that returns the Iterator type with a parameter empty. Follow the Sequence protocol and call this property in the makeIterator method.
let _makeIterator: () -> Iterator func makeIterator() -> Iterator { return _makeIterator() }
The constructor of MAnySequence is where magic works, which takes any sequence as parameters:
init<Seq: Sequence>(_ seq: Seq) where == Element {
Next, you need to wrap the function of this sequence in the constructor:
_makeIterator = {
How to generate an iterator? Request Seq sequence generation:
var iterator = ()
Next, we use the custom iterator to wrap the sequence of the iterator. The wrapped _next attribute will be called in the next() method of the iterator protocol:
return Iterator(_next: { () }) } } }
Next, show how to use MAnySequence:
func printInts(_ seq: MAnySequence<Int>) { for elt in seq { print(elt) } } let array = [1, 2, 3, 4, 5] printInts(MAnySequence(array)) printInts(MAnySequence(array[1 ..< 4]))
It works correctly, it's great!
This function-based type erasure method is particularly practical when it is necessary to wrap a small part of the function as part of a larger type, so that there is no need for a separate class to implement this part of the function of the erased type.
For example, you want to write some code now that works for various collection types, but what it really needs to be able to do with those collections is to get the count and perform integer subscripts starting from zero. For example, accessing the tableView data source. It might look like this:
class GenericDataSource<Element> { let count: () -> Int let getElement: (Int) -> Element init<C: Collection>(_ c: C) where == Element, == Int { count = { } getElement = { c[$0 - ] } } }
GenericDataSource Other codes can manipulate the incoming collection by calling count() or getElement() . And the collection type will not be allowed to destroy the GenericDataSource generic parameters.
Conclusion
Type erasing is a very useful technique that can be used to prevent generic intrusion into code and to ensure that the interface is simple and clear. By wrapping the underlying type, split the API from specific functions. This can be achieved by using abstract public superclasses and private subclasses or wrapping the API in a function. For simple cases where only some functions are required, function type erasing is extremely effective.
The Swift standard library provides several types of erasing that can be directly exploited. For example, AnySequence wraps a Sequence, as its name suggests, AnySequence allows you to iterate over a sequence without knowing the specific type of the sequence. AnyIterator is also a type erase type, which provides an iterator for type erase. AnyHashable is also a type erased type, which provides access to Hashable types. Swift also has many collection-based erase types that you can check by searching for Any. Type erase types are also designed for the Codable API in the standard library: KeyedEncodingContainer and KeyedDecodingContainer. They are container protocol type wrappers that can be used to implement Encode and Decode without knowing the underlying specific type information.
Summarize
The above is the entire content of this article. I hope that the content of this article has a certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support.
Author: Mike Ash, original link, original date: 2017-12-18
Translator: rsenjoyer; Proofreader: Yousanflics, numbbbbbb; Finalized: Forelax