agreement
When we use generic types, we usually use protocols to constrain the behavior of generic parameters. There are many reasons why you should, and here are some of the most common examples:
- With protocols, you can build algorithms that rely on numbers (rather than specific numeric types such as Int, Double, etc.) or collection types. In this way, all types that implement the protocol have the capabilities provided by the heart algorithm.
- Protocols also allow you to abstract the implementation details behind the code interface. You can program to a protocol and then have different types implement that 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 such as Linux, macOS, or iOS.
- You can also use protocols to make your code more testable. More specifically, when you implement a function based on a protocol rather than a specific type, it is easy to replace this with types representing various test results in test cases.
In Swift, a protocol represents a set of formally requested requirements. For example, the Equatable protocol requires that implemented types provide the == operator. These requirements can be normal methods, initialization methods, association types, properties, and inherited protocols. Some protocols also have requirements that cannot be expressed in the Swift type system. For example, the Collection protocol requires the subscript operator to access elements with a time complexity of 0(1). (You can also violate this requirement if the algorithm time complexity is not O(1).
Main features of the Swift protocol.
- Protocols can extend themselves with new functionality.
- Protocols can add apis that require additional constraints through conditional Extensions.
- Protocols can inherit from other protocols.
- Protocols can be combined to form new protocols.
- Sometimes, the implementation of one protocol depends on the implementation of other protocols.
- A protocol can also declare an association type. Implementing the type of this protocol requires defining the specific type of the association type.
Agreement witness
struct Eq<A> {
let eq:(A.A) - >Bool
}
Copy the code
Now we can create different Eq instances for comparing different concrete types (for example: Int). We call these instances explicit witnesses representing the judgment of equality:
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
Here’s an implementation of allEqual that replaces the Explicit witness by protocol:
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
Instead of the generic argument A, we can use the implicit generic argument Self, which represents the type that implements the protocol:
extension Equatable {
static func notEqual(_ l:Self._ r:Self) -> Bool {
return !(l = = r)
}
}
Copy the code
Conditional Conformance
Array implementation of Equatable in the library:
extension Array: Equable where Element: Equatable {
static func = = (lhs: [Element].rhs: [Element]) -> Bool {
fatalError("Implementation left out")}}Copy the code
Agreement inheritance
Swift also supports protocol inheritance. For example, a type that implements Comparable must also implement Equatable. This is called refinging. Comparable, in other words, improves Equatable:
public protocol Comparable: Equatable { static func < (lhs: Self.rhs: Self) -> Bool / /... }
Copy the code
Design using protocols
As an example of a drawing protocol, first, define a protocol that requires the implementation of an ellipse and rectangle interface:
protocol DrawingContext {
mutating func addEllipse(rect: CGRect.fill: UIColor)
mutating func addRectangle(rect: CGRect, fill: UIColor)
}
Copy the code
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
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 the Swift protocol is Protocol Extension. Once you know how to draw an ellipse, you can add an extension to draw a circle with a point as its center.
extension DrawingContexnt { 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
Create another extension for DrawingContext and add a method to draw a blue circle in the yellow square:
extension DrawingContext { mutating func drawingSomething(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 the DrawingContext extension, we can call it from either SVG or CGContext instances. This is an approach that runs through the Swift standard library: as long as you implement the few methods required by the protocol, you can reap all the functionality of the protocol “for free” by extending it.
Custom protocol Extension
Methods added to a protocol by extension are not part of the protocol constraint. In some cases, this can lead to unexpected results.
Only the methods in the protocol witness can be dynamically dispatched to an implementation corresponding to a specific type, because only the information in the witness is available at run time. In a generic context, unconstrained methods in the calling protocol are always statically dispatched to the implementation in the protocol extension.
To get dynamic dispatch behavior, we should make addCircle part of the protocol constraint. Thus, the implementation of addCircle in the protocol extension becomes the default implementation of the protocol constraint. A protocol method with a default implementation is sometimes called a customization point in the Swift community. The type implementing the protocol receives a default implementation of the method and has the right to decide whether to override it.
Protocol combinations
Protocols can be grouped together.
typealias Codable = Decodable & Encodable
Copy the code
Agreement inheritance
There can also be an inheritance relationship between protocols. In fact, typealias Codable = Encodable & Decodable is grammatically identical to protocol Codable: Encodable & Decodable. The alias is a little simpler to write, and it tells us more explicitly that Codable is just a combination of the two protocols, and does not add any new methods to the result of the combination.
Protocol and association type
Some protocols require more than just methods, properties, and initialization methods to be constrained; they also expect certain conditions to be met for the types associated with them. This can be done with associated types.
For example, re-implement a small UIKit state recovery mechanism with protocol association types.
In UIKit, state recovery requires reading the view controller and view architecture and serializing their state while the app is hanging. The next time the App loads, UIKit tries to restore the state of the App.
protocol ViewController {}
Copy the code
protocol Restorable {
associatedtype State: Codable
var state: State { get set}}Copy the code
Create a view controller that displays messages. The state of the view controller consists of an array of messages and the current roll-down position, which we define as a Codable embedded type:
class MessagesVC: ViewController.Restorable { typealias State = MessagesState struct MessagesState: Codable { var messgaes: [String] = [] var scrollPosition: CGFloat = 0 } var state: MessagesState = MessgaesState()}
Copy the code
Conditional protocol implementation based on association type
Some types implement a protocol only under certain conditions.
extension Range: Sequence
where Bound: Strideable.Bound.Stride: SignedInteger
Copy the code
beings
Strictly speaking, protocols cannot be used as concrete types in Swift; they can only be used to constrain generic parameters. Surprisingly, the following code does compile:
let Context: DrawingContext = SVG(a)Copy the code
When we use a protocol as a concrete type, the compiler creates a wrapper type called Existential for the protocol. Let context:DrawingCon is essentially a syntactic sugar like let context: Any
. Although such a 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.
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 verify 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 depend 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.
MemoryLayout<Codable>.size / / 48
let codables: [Codable] = [Int(42), Double(42), "fourtytwo"] // Occupies 144(48 x 3) bytes
Copy the code
Existing body and association type
In Swift 5, existents are only for protocols that do not have associated types and Self constraints.
let collections: [Collection] = ["foo"[1]]
// Error: 'Collection' can only be used as a generic parameter constraint
// Because it contains Self or the association type convention.
Copy the code
We cannot use Collection without specifying the association type Element.
Type eliminator
Although we cannot create an entity for a protocol with Self or associated type constraints, we can write a function that does something similar called: Type Erasers.
let seq = [1.2.3].lazy.filter { $0 > 1 }.map { $0 * 2 }
Copy the code
The type of SEq is LazyMapSequence
, Int>.
We want to eliminate the type details in the result and just get a sequence with an Int element. 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 like a sequence, it comes at a cost: AnySequence introduces an extra layer of indirection that is slower than using the hidden primitive types directly.
The standard library provides type eliminators for many protocols, such as AnyCollection and AnyHashable.
Let’s implement a simple type eliminator for the previously defined Restorable protocol.
class AnyRestorableBoxBase<State: Codable> :Restorable {
internal init(a) {}
public var state: State {
get { fatalError()}set { fatalError()}}}Copy the code
class AnyRestorableBox<R: Restorable> :AnyRestorableBoxBase<R.State> {
var r: R
int(_ r: R) {
self.r = r
}
override var state: R.State {
get { return r.state }
set { r.state = newValue }
}
}
Copy the code
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
Protocol implementation lags behind type definition
A major feature of the protocol in Swift is that a type’s implementation of the protocol can later lag behind the type definition itself.