The article is from collated notes on objccn. IO/related books. “Thank you objC.io and its writers for selflessly sharing their knowledge with the world.”
When generic types are used, protocols are often used to constrain the behavior of generic parameters.
- With the protocol, you can build an algorithm that relies on numbers (rather than a specific numeric type such as Int, Double, etc.) or collection types. In this way, all types that implement the protocol have the capabilities provided by the new algorithm.
- The protocol also abstracts the implementation details behind the code interface, allows you to program against the protocol and then have different types implement the protocol. For example, a drawing program that uses the Drawable protocol can render Graphics using either SVG or Core Graphics. Similarly, cross-platform code can use a Platform protocol and then be implemented by a type like Linux, macOS, or iOS.
- You can use protocols to make your code more testable. More specifically, when a function is implemented based on a protocol rather than a specific type, it is easy to replace this part in a test case with a type that represents the various results to be tested.
In Swift, a protocol represents a set of formally requested requirements. For example, the Equatable protocol requires that the type implemented provide the == operator. These requirements can be common methods, initialization methods, association types, attributes, and inherited protocols. Some protocols also have requirements that cannot be expressed in the Swift type system. For example, the Collection protocol requires O(1) time to access elements through the subscript operator (but you can violate this requirement if the algorithm is not O(1), just specify it in the method documentation).
The protocol can extend new functionality on its own. The simplest example is Equatable, which requires that the type implemented provide the == operator. It then provides based on the implementation of ==! = function of operator. Similarly, the Sequence protocol does not require many methods (it only requires a method to produce an iterator), but it can be extended to include a large number of available methods.
Protocols can add apis that require additional constraints through conditional extensions. For example, in the Collection protocol, the Max () method is provided only if Element implements Comparable.
Protocols can inherit from other protocols. For example, Hashable requires that the type implemented must also implement the Equatable protocol. Similar RangeReplaceableCollection inherited from the Collection, and Collection inherited from the Sequence. In other words, we can build a protocol hierarchy.
In addition, protocols can be combined to form new protocols. For example, Codable is an alias for Encodable and Decodable protocols.
Sometimes, the implementation of one protocol depends on the implementation of other protocols. For example, the array type implements Equatable if and only if the Element type implements Equatable in the array. This is called conditional conformance: Conditions that Array implements Equatable, and Element implements Equatable.
The protocol can also declare the association type, and the specific type corresponding to the association type needs to be defined to implement the protocol type. For example, IteratorProtocol defines an association type, Element. Each type that implements IteratorProtocol defines its own Element type.
Each protocol introduces an additional layer of abstraction, which sometimes makes it harder to understand the code. Sometimes, however, using protocols can greatly simplify code.
Agreement witness
Understand how agreements work. Assuming Swift does not have a protocol feature, if you wanted to add a method to an Array to determine whether all elements are equal, without the Equatable protocol, you would have to pass the method a comparison function:
extension Array {
func allEqual(_ compare: (Element.Element) - >Bool) -> Bool {
guard let f = first else { return true }
for el in dropFirst() {
guard compare(f, el) else { return false}}return true}}Copy the code
To make things more formal, we can create a wrapper based on allEqual’s parameters to make it more explicit what equality comparison means:
struct Eq<A> {
let eq: (A.A) - >Bool
}
Copy the code
Now you can create different Eq instances for comparing different concrete types (for example :Int). Call these examples explicit witnesses representing equal judgments:
let eqInt: Eq<Int> = Eq { $0 = = The $1 }
Copy the code
Next, we can use Eq to transform the previous allEqual implementation. Use the generic Element type to express the types of all elements to be compared:
extension Array {
func allEqual(_ compare: Eq<Element>) -> Bool {
guard let f = first else { return true }
for el in dropFirst() {
guard compare.eq(f, el) else { return false}}return true}}Copy the code
Although Eq looks a bit obscure here, it shows how the protocol works behind the scenes: Once you add an Equatable constraint to a generic type, create an instance of the corresponding concrete type, and a protocol witness is passed to it. In the case of Equatable, the witness carries the == operator, which is used to compare two values. Depending on the type to be created, the compiler automatically passes in protocol witnesses. The following is an allEqual implementation that replaces explicit witnesses by agreement:
extension Array where Element: Equatable {
func allEqual(a) -> Bool {
guard let f = first else { return true }
for el in dropFirst() {
guard f = = el else { return false}}return true}}Copy the code
You can also add an extension to Eq. For example, we can implement a method to determine whether two elements are unequal as long as we define methods to compare them:
extension Eq {
func notEqual(_ l: A._ r: A) -> Bool {
return !eq(l,r)
}
}
Copy the code
This is similar to adding functionality to a protocol through extensions: since the EQ method definitely exists, more functionality can be built on top of it. Thus, an extension that adds the same functionality to Equatable is almost the same as the notEqual implementation above. However, instead of the generic argument A, we can use the implicit generic argument Self to represent the type that implements the protocol:
extension Equatable {
static func notEqual(_ l: Self._ r: Self) -> Bool {
return !(l = = r)
}
}
Copy the code
This is exactly what the standard library implements for Equatable! Method of the = operator.
Conditional Conformance
To implement Eq for comparing arrays, you need a way to compare two elements in an array. This time, define eqArray as a function and pass it explicit witnesses:
func eqArray<El> (_ eqElement: Eq<El>) -> Eq"[El] > {return Eq { arr1, arr2 in
guard arr1.count = = arr2.count else { return false }
for (l, r) in zip(arr1, arr2) {
guard eqElement.eq(l, r) else { return false}}return true}}Copy the code
EqArray explains how conditional protocol implementation works in Swift. For example, here is an implementation of Array to Equatable in the library:
extension Array: Equatable where Element: Equatable {
static func = =(lhs: [Element].rhs: [Element]) -> Bool {
fatalError("Implementation left out")}}Copy the code
Here, adding the Equatable constraint to the Element is essentially the same as passing eqElement to the eqArray function. In the Array extension, we can directly compare the values of two elements using the == operator. The major difference between these two approaches is that with protocol constraint types, the compiler automatically passes a protocol witness.
Agreement inheritance
Swift also supports protocol inheritance. For example, a type that implements Comparable must also implement Equatable. It’s called refining, in other words, Comparable improves Equatable:
public protocol Comparable : Equatable {
static func < (lhs: Self.rhs: Self) -> Bool // ...
}
Copy the code
This idea of protocol refinement can also be expressed in the previously hypothetical Swift version without protocol features. To do this, create an explicit witness for Comparable that contains Equatable’s witness and a lessThan function:
struct Comp<A> {
let equatable: Eq<A>
let lessThan: (A.A) - >Bool
}
Copy the code
This time, the definition of Comp tells us how a witness to a new protocol inherited from another protocol works. Thus, we can use Eq and lessThan in the Comp extension:
extension Comp {
func greaterThanOrEqual(_ l: A._ r: A) -> Bool {
return lessThan(r, l) || equatable.eq(l, r)
}
}
Copy the code
This mode of passing explicit witnesses is very helpful for our protocol support inside the compiler. And that, in turn, helps us find ideas when we’re stuck with protocol.
However, explicitly passing witnesses and using protocol constraint types are not exactly the same. There can be countless explicit witnesses of the same type, but a type can only provide one implementation of the method of the protocol constraint. And, unlike explicit witnesses, which can be passed manually by parameters, protocol witnesses are passed automatically.
If multiple implementations of a protocol are allowed, the compiler needs some way to find the most appropriate implementation for the current environment. If this process is coupled with conditional protocol implementation, it becomes even more complicated. To avoid this complexity, Swift doesn’t allow us to do this.
Design using protocols
Look at an example of a drawing protocol. There are two specific types that implement this protocol: Graphics can be drawn as SVG or rendered into the Graphics Context of Apple’s own Core Graphics framework. Start by defining a protocol that requires implementing an interface for drawing ellipses and rectangles:
protocol DrawingContext {
mutating func addEllipse(rect: CGRect.fill: UIColor)
mutating func addRectangle(rect: CGRect.fill: UIColor)
}
Copy the code
Let CGContext implement this protocol:
extension CGContext: DrawingContext {
func addEllipse(rect: CGRect.fill fillColor: UIColor) {
setFillColor(fillColor.cgColor)
fillEllipse(in: rect)
}
func addRectangle(rect: CGRect.fill fillColor: UIColor) {
setFillColor(fillColor.cgColor)
fill(rect)
}
}
Copy the code
Similarly, let SVG implement this protocol. Convert the rectangle to a series of XML attributes, and convert the UIColor to a hexadecimal string:
extension SVG: DrawingContext {
mutating func addEllipse(rect: CGRect.fill: UIColor) {
var attributes: [String:String] = rect.svgEllipseAttributes
attributes["fill"] = String(hexColor: fill)
append(Node(tag: "ellipse", attributes: attributes))
}
mutating func addRectangle(rect: CGRect.fill: UIColor) {
var attributes: [String:String] = rect.svgAttributes
attributes["fill"] = String(hexColor: fill)
append(Node(tag: "rect", attributes: attributes))
}
}
Copy the code
Protocol extensions
A key feature of Swift protocol is Protocol extension. Once you know how to draw an ellipse, you can add an extension to draw a circle around a point. For example, add the following extension to DrawingContext:
extension DrawingContext {
mutating func addCircle(center: CGPoint.radius: CGFloat.fill: UIColor) {
let diameter = radius * 2
let origin = CGPoint(x: center.x - radius, y: center.y - radius)
let size = CGSize(width: diameter, height: diameter)
let rect = CGRect(origin: origin, size: size)
addEllipse(rect: rect.integral, fill: fill)
}
}
Copy the code
To use it, we can create another extension to DrawingContext, adding a method to draw a blue circle in a yellow square: DrawingContext
extension DrawingContext {
mutating func drawSomething(a) {
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
addRectangle(rect: rect, fill: .yellow)
let center = CGPoint(x: rect.midX, y: rect.midY)
addCircle(center: center, radius: 25, fill: .blue)
}
}
Copy the code
By defining this method in an extension of DrawingContext, it can be called from an SVG or CGContext instance. This is an approach that runs through the Implementation of the Swift standard library: as long as you implement the few methods required by the protocol, you get all the functionality that the protocol extends “for free.”
Custom Protocol Extension
Methods added to a protocol by extension are not part of a protocol constraint. In some cases, this can lead to unexpected results. Going back to the previous example, you want to use SVG’s built-in support for circles, which means that circles should be drawn in SVG as circles, not as ellipses. So, in our SVG implementation, we added an addCircle method:
extension SVG {
mutating func addCircle(center: CGPoint.radius: CGFloat.fill: UIColor) {
let attributes = [
"cx": "\(center.x)"."cy": "\(center.y)"."r": "\(radius)"."fill": String(hexColor: fill),
]
append(Node(tag: "circle", attributes: attributes)) }
}
Copy the code
When an SVG variable is created and the addCircle method is called, it behaves as expected:
var circle = SVG()
circle.addCircle(center: .zero, radius: 20, fill: .red) circle
/ * < SVG > < circle cx = "0.0" cy = "0.0" the fill = "# ff0000" r = "20.0" / > < / SVG > * /
Copy the code
However, when we call drawSomething() defined on Drawing (this method calls addCircle), the addCircle extension for SVG will not be called. As you can see in the following result, the SVG syntax contains the ellipse tag instead of the expected circle:
var drawing = SVG() drawing.drawSomething() drawing
/ * < SVG > < the rect fill = "# ffff00" height = "100.0" width = "100.0" x = "0.0" y = "0.0" / > < the ellipse cx = "50.0" cy = "50.0" the fill = # 0000 ff "" Rx = "25.0" ry = "25.0" / > < / SVG > * /
Copy the code
This behavior is surprising compared to the way protocol constraints are implemented. To understand what happens, write drawSomething as a generic global function. It says exactly the same thing as the implementation in the protocol extension:
func drawSomething<D: DrawingContext> (context: inout D) {
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
context.addRectangle(rect: rect, fill: .yellow)
let center = CGPoint(x: rect.midX, y: rect.midY)
context.addCircle(center: center, radius: 25, fill: .blue)
}
Copy the code
Here, the generic parameter D is a type that implements DrawingContext. This means that when DrawingContext is called, compile time automatically passes a protocol witness to DrawingContext. This witness only has all the methods of the protocol constraints, namely addRectangle and addEllipse. Since addCircle is only a method defined in the extension, it is not part of the protocol constraints and therefore is not in the witness.
The key to this problem is that only methods in the protocol witness can be dynamically dispatched to a specific type of implementation, because only information in the witness is available at runtime. In a generic context, unconstrained methods in a calling protocol are always statically dispatched to an implementation in a protocol extension.
As a result, when addCircle is called from drawSomething, the call is always statically dispatched to the implementation of the protocol extension. The compiler was unable to generate the necessary dynamically distributed code to invoke the implementation we added to the SVG extension. To achieve dynamic distributive behavior, we should make addCircle part of the protocol constraints:
protocol DrawingContext {
mutating func addEllipse(rect: CGRect.fill: UIColor)
mutating func addRectangle(rect: CGRect.fill: UIColor)
mutating func addCircle(center: CGPoint.radius: CGFloat.fill: UIColor)
}
Copy the code
Thus, the implementation of addCircle in the protocol extension becomes the default implementation of the protocol constraint. With this default implementation, the code that previously implemented DrawingContext does not need to be modified and can still be compiled. Now that addCircle is part of the protocol, it becomes a protocol witness. When we call the drawSomething method on an SVG object, the expected addCircle implementation is called:
var drawing2 = SVG() drawing2.drawSomething() drawing2
/ * < SVG > < the rect fill = "# ffff00" height = "100.0" width = "100.0" x = "0.0" y = "0.0" / > < circle cx = "50.0" cy = "50.0" the fill = # 0000 ff "" R = "25.0" / > < / SVG > * /
Copy the code
A protocol method with a default implementation is sometimes called a customization point in the Swift community. The implementation protocol type receives a default implementation of the method and decides whether to override it. Such customization points can be found all over the standard library. An example is to calculate the distance(from:to:) between two elements in a set. The time complexity of this method is O(n) by default, because it traverses all positions between two elements. Since Distance (from:to:) is also a customization point, the default implementation can be overridden for types that provide a more efficient implementation, such as Array.
Protocol combinations
Protocols can be combined. One example in the standard library is Codable, which is another name for Encodable & Decodable:
typealias Codable = Decodable & Encodable
Copy the code
This means that by writing the following function, we can use the methods of both protocol constraints in its implementation via value:
func useCodable<C: Codable> (value: C) {
// ...
}
Copy the code
This combination is called a new protocol for Encodable & Decodable and is called Codable.
In the previous drawing example, you might want to render strings with attributes that contain subranges of formatting, such as bold, font, and color. However, SVG does not provide native support for attribute strings (Core Graphics does). Instead of adding a new method to DrawingContext, we create a new protocol:
protocol AttributedDrawingContext {
mutating func draw(_ str: NSAttributedString.at: CGPoint)
}
Copy the code
This way, you can just have CGContext implement the protocol without adding the same support to SVG. Also, you can combine the two protocols. Add an extension to DrawContext, for example, requirements to realize its type also AttributedDrawingContext:
extension DrawingContext where Self: AttributedDrawingContext {
mutating func drawSomething2(a) {
let size = CGSize(width: 200, height: 100)
addRectangle(rect: .init(origin: .zero, size: size), fill: .red)
draw(NSAttributedString(string: "hello"), at: CGPoint(x: 50,y: 50))}}Copy the code
Alternatively, you can write a function with generic parameter constraints. This function is semantically the same as the method in the extension:
func drawSomething2<C: DrawingContext & AttributedDrawingContext> (_ c: inout C)
{
// ...
}
Copy the code
Protocol composition is a powerful syntactic tool that allows you to add operations to a protocol that are not supported by all types that implement the protocol.
Agreement inheritance
In addition to combining protocols as in the previous section, protocols can also have inheritance relationships. For example, the AttributedDrawingContext defined earlier could also be written like this:
protocol AttributedDrawingContext: DrawingContext {
mutating func draw(_ str: NSAttributedString.at: CGPoint)
}
Copy the code
This definition requires that the type that implements AttributedDrawingContext must also implement the DrawingContext.
Protocol inheritance and protocol combination have their own application scenarios. For example, the Comparable protocol is inherited from Equatable. This means that a type that implements Comparable simply implements the < operator, and it can automatically add definitions such as >= and <= operators. In the case of Codable, it makes no sense to make Encodable inherit from Decodable, or vice versa. But there’s no problem defining a new protocol called Codable to inherit both Encodable and Decodable.
Typealias Codable = Encodable & Decodable: protocol cocodable: Encodable, Decodable {} The alias is a little more concise, though, and makes it clear that Codable is just a combination of the two protocols. Codable does not add any new methods to the group.
Protocol and association type
Some protocols need to constrain more than just methods, attributes, and initializers; they also want certain conditions to be met by some of the types associated with them. This can be done with associated types.
Association types are not commonly used in our own code, but they are ubiquitous in the standard library. One of the shortest examples is the IteratorProtocol protocol in the standard library. It has an association type that represents the iterated element and a method that accesses the next element:
protocol IteratorProtocol {
associatedtype Element
mutating func next(a) -> Element?
}
Copy the code
The Collection protocol has five association types, most of which have default values. For example, the default value for the correlation type SubSequence is Slice
. Of course, when a type implements a Collection, this is another customization point: you can choose to use the default implementation to reduce development effort. This type is often overridden for performance or for more convenient collection types (e.g., String uses Substring for SubSequence).
Re-implement a small UIKit state recovery mechanism through protocol association types. In UIKit, state recovery requires reading the view controller and its schema and serializing their state when the app hangs. The next time the App loads, UIKit will try to restore the state of the App.
Next, we’ll use protocols, rather than a class inheritance structure, to represent view controllers. In a real implementation, the ViewController protocol might contain many methods, but for simplicity’s sake, we’ll leave it empty:
protocol ViewController {}
Copy the code
To recover a particular view controller, you need to be able to read and write its state, which is Codable for encoding and decoding. Since this state is associated with a specific view controller, it can be defined as an association type:
protocol Restorable {
associatedtype State: Codable
var state: State { get set}}Copy the code
To demonstrate, create a view controller that displays messages. The state of this view controller consists of an array of messages and the current scroll position. We define it as a Codable inline type:
class MessagesVC: ViewController.Restorable {
typealias State = MessagesState
struct MessagesState: Codable {
var messages: [String] = []
var scrollPosition: CGFloat = 0
}
var state: MessagesState = MessagesState()}Copy the code
In fact, there is no need to declare TypeAlias State in Restorable code. The compiler is smart enough to infer the type of state from the state attribute. You can also rename MessagesState to State and everything will still work.
Implementation of conditional protocol based on association type
Some types implement a protocol only under certain conditions. As we saw earlier in the conditional Protocol Implementation section, Array is a type Equatable only if the type of the element in the Array implements Equatable. Information about association types can also be used in conditions that constrain protocol implementations. For example, Range has a generic argument Bound. If and only if Bound implements the Strideable protocol and the Stride in Bound (which is an association type of Strideable) is a SignedInteger protocol implementation, Range is a type that implements a Sequence:
extension Range: Sequence
where Bound: Strideable.Bound.Stride: SignedInteger
Copy the code
SplitViewController, which represents its two child view controllers with two generic arguments:
class SplitViewController<Master: ViewController.Detail: ViewController> {
var master: Master
var detail: Detail
init(master: Master.detail: Detail) {
self.master = master
self.detail = detail
}
}
Copy the code
Assuming that the split view controller has no state of its own, we can combine the states of its two child view controllers as the split view controller state. So maybe the most natural thing to think about is this :var state: (master.state, detail.state). Unfortunately, the tuple type is not Codable, and there is no way to add Codable support to it through a conditional protocol implementation (in fact, tuples don’t implement any protocols). So I had to write my own generic structure, Pair, and add Codable conditional protocol implementation to it:
struct Pair<A.B> :Codable where A: Codable.B: Codable {
var left: A
var right: B
init(_ left: A._ right: B) {
self.left = left
self.right = right
}
}
Copy the code
Finally, in order for SplitViewController to implement Restorable, we must require that the Master and Detail also implement types of Restorable. Instead of keeping a separate copy of the combined state in a SplitViewController, it can be calculated directly from its two child view controllers. By omitting this local variable, we immediately pass state changes to the two child controllers:
extension SplitViewController: Restorable
where Master: Restorable.Detail: Restorable
{
var state: Pair<Master.State.Detail.State> {
get {
return Pair(master.state, detail.state)
}
set {
master.state = newValue.left
detail.state = newValue.right
}
}
}
Copy the code
A protocol can only be implemented once for any type. This means that we can no longer add a protocol implementation condition such as Master implementing Restorable, but Detail doesn’t (or vice versa).
beings
Strictly speaking, protocols cannot be used as a concrete type in Swift; they can only be used to constrain generic parameters. But the following code will compile (using the DrawingContext protocol in the example above):
let context: DrawingContext = SVG(a)Copy the code
When we use the protocol as a concrete type, the compiler creates a wrapper type for the protocol, called An Existential. Let context: DrawingContext is essentially a syntactic sugar like let context: Any
. Although this syntax does not exist, the compiler creates a (32-byte) Any box in which it adds an 8-byte protocol witness for each protocol implemented by the type. We can verify this result with the following code:
MemoryLayout<Any>.size / / 32
MemoryLayout<DrawingContext>.size / / 40
Copy the code
The box created for the protocol is also called an Existential Container. This is something the compiler must do because it needs to confirm the size of the type at compile time. Different types have their own size difference (for example: all the classes are the size of a pointer, and the size of the structure and the enumeration is dependent on their actual content), these types of implements a protocol, wrap agreement in the presence of the container body can keep the size of the type of fixed, the compiler can determine the object’s memory layout.
You can see that the size of the body container grows as the type implementation protocol increases. For example, Codable is a combination of Encodable and Decodable, so we can expect 32-byte Any containers for Codable bodies, plus two 8-byte protocol witnesses:
MemoryLayout<Codable>.size / / 48
Copy the code
When we create a Codable array, the compiler can verify that each element is 48 bytes in size, regardless of the specific type. For example, the following array of three elements will take up 144 bytes:
let codables: [Codable] = [Int(42), Double(42), "fourtytwo"]
Copy the code
The only thing you can do for the elements in the Codables array is call the apis in Encodable and Decodable. Or is and other runtime type conversions). Because the specific type of the element is hidden by the existence container.
Sometimes existential bodies and generic parameters with type constraints are interchangeable. Consider the following two functions:
func encode1(x: Encodable){}func encode2<E: Encodable> (x: E){}Copy the code
Although both functions can be called with an Encodable type, they are not identical. For Encode1, the compiler wraps parameters into Encodable existence containers. This wrapper not only incurs some performance overhead, but also requires additional memory space if the values to be wrapped are too large to be stored directly in the body. Perhaps more importantly, it also prevents the compiler from making further optimizations, since all method calls to the wrapped type can only be done through the protocol witness table in the body.
For generic functions, the compiler can generate a specialized version of some or all of the parameter types passed to Encode2. The performance of these specialized versions is exactly the same as if we had manually reloaded Encode2 for these types. The disadvantages of the generic approach compared to Encode1 are longer compilation times and larger binaries.
For most code, the performance overhead of body is not an issue, but when you’re writing performance-critical code, take this into account. If you call these two encode functions thousands of times in a loop, encode2 is much faster.
Existence body and association type
In Swift 5, the existence body only applies to protocols that do not have associative types and Self constraints
let collections: [Collection] = ["foo"[1]]
// error: 'Collection' can only be used for generic parameter constraints
// Because it contains Self or the associative type convention.
Copy the code
The above code does not make sense: you cannot use Collection without specifying the association type Element. For now, this is a hard and fast rule in Swift, but in future Swift releases it is possible to write code like this:
// Is not a true Swift syntax
let collections: [any Collection where .Element = = Character] = ["foo"["b"]]
Copy the code
For protocols that contain Self constraints, the restriction is similar. For example, consider the following code:
let cmp: Comparable = 15 // Error compiling
Copy the code
The operators defined in Comparable (and those inherited from Equatable) want the types of the two parameters used for comparison to be exactly the same. If you allow variables of type Comparable to be defined, you might use the Comparable API to compare them, for example:
(15 as Comparable) < ("16" as Comparable)
// Error: the binary operator '<' cannot be used for two 'Comparable' operands.
Copy the code
However, this makes no sense at all, because it is impossible to compare strings and integers directly. Therefore, the compiler disallows generating bodies for protocols that contain associative type constraints (or protocols that use Self, which is essentially an associative type).
Type eliminator
Although you cannot create an existence body for a protocol with Self or associated type constraints, you can write a function that performs a similar function called Type Erasers.
let seq = [1.2.3].lazy.filter { $0 > 1 }.map { $0 * 2 }
Copy the code
Its type is LazyMapSequence
, Int>. As more operations are concatenated, this declaration becomes more complex. Sometimes, you want to eliminate the details of the type in the result and just get a sequence of ints. While you can’t express this idea with existential bodies, you can hide the original type with AnySequence:
let anySeq = AnySequence(seq)
Copy the code
The type of anySeq is AnySequence
. Although this looks much simpler and works just like a sequence, it comes at a cost :AnySequence introduces an extra layer of indirection, which is slower than using the hidden primitive type directly.
The library provides type eliminators for many protocols, such as AnyCollection and AnyHashable. We implement a simple type eliminator for the Restorable protocol we defined earlier.
Might write an AnyRestorable like the one below. But it doesn’t get the job done. Because the generic parameter R directly exposes the protocol to hide, this version of AnyRestorable is as good as a sham:
struct AnyRestorable<R: Restorable> {
var restorable: R
}
Copy the code
In fact, you would expect AnyRestorable’s generic parameter to reflect State. In order for AnyRestorable to implement Restorable, we also need to provide the state property. To do this, we use the same implementation as the standard library: it uses three classes to implement a type eliminator. First, we create a class that implements Restorable: AnyRestorableBoxBase. Accessing its state property directly causes a fatalError. Because this class is part of the implementation details, objects of this type should never be created directly:
class AnyRestorableBoxBase<State: Codable> :Restorable {
internal init(a){}public var state: State {
get { fatalError()}set { fatalError()}}}Copy the code
Second, create a derived class of AnyRestorableBoxBase with a generic parameter R that implements Restorable. Here, the trick that makes the type eliminator work is to restrict AnyRestorableBoxBase’s generic arguments to being of the same type as r.state:
class AnyRestorableBox<R: Restorable> :AnyRestorableBoxBase<R.State> {
var r:R
init(_ r: R) {
self.r = r
}
override var state: R.State {
get { return r.state }
set { r.state = newValue }
}
}
Copy the code
This derivation means that we can create an AnyRestorableBox instance, but use it as an AnyRestorableBoxBase. Since AnyRestorableBoxBase implements Restorable, it can be used as a Restorable directly. Finally, we create a wrapper class, AnyRestorable, to hide AnyRestorableBox:
class AnyRestorable<State: Codable> :Restorable {
let box: AnyRestorableBoxBase<State>
init<R> (_ r: R) where R: Restorable.R.State = = State {
self.box = AnyRestorableBox(r)
}
var state: State {
get { return box.state }
set { box.state = newValue }
}
}
Copy the code
In general, when writing a type eliminator, make sure it includes all methods of protocol constraints. Although the compiler can help with this, it lets protocol methods with default implementations pass. In a type eliminator, you can’t rely on these default implementations, but always forward method calls to hidden primitives because they can be customized.
Protocol implementations that lag behind type definitions
A major feature of the protocol in Swift is that the implementation of a protocol by a type can lag behind the type definition itself. For example, at the beginning of this chapter, CGContext implements Drawable. When having a type implement a protocol, we should ensure that we are either the owner of the type or the owner of the protocol (or both). However, having a type that doesn’t belong to you implement a protocol that doesn’t belong to you is not recommended.
For example, CLLocationCoordinate2D in the Core Location framework does not implement the Codable protocol. Although it would be easy to add this support, if Apple decided to add official Codable support to CLLocationCoordinate2D, their own implementation would not compile. In this case, Apple might choose a different implementation, and as a result, we might not be able to deserialize existing file formats.
Implementation conflicts can also occur when the same type implements the same protocol in different packages. This has happened before with SourceKit-LSP and SwiftPM. They are both Codable for Range, but on different terms. The result is Codable for Range in Swift 5.
As a solution to these potential problems, create a wrapper type and add a conditional protocol implementation to it. For example, you can create a structure that contains CLLocationCoordinate2D and make it Codable.