Let’s start by reviewing the history of changes to generic support in Swift to see what features generics now support in Swift

Swift generics is an important feature of Swift language. It has been mentioned in all WWDC conferences and there are many references on the Internet. Some design ideas for generic features were discussed at this meeting

The importance of generics to Swift

Consider one of the following collection types

struct Buffer {  
    var count: Int

    subscript(at: Int) -> Any { 
        // get/set from storage
    }
}
Copy the code

For such a collection type, we cannot define the variable type corresponding to its get/set methods. At best, we can define a universal type (such as id in OC or void * in C++). Swift also has a universal type of Any, but this can lead to a very bad development experience, as shown in the following example

var words: Buffer = ["subtyping","ftw"] 

// I know this array contains strings
words[0] as! String

// Uh-oh, now it doesn’t!
words[0] = 42  
Copy the code

In the case of words variables, you might have been using them as an array of strings, but in fact you might have stuffed a non-string variable somewhere else, resulting in a forced unpack failure that caused the program to crash, which is a frustrating experience.

In fact, for the example above, the memory management problem is even more pronounced. For an integer array, for example, the memory layout is very compact

If it’s aAnyThe memory footprint of an array of types becomes very large, because it is a huge waste of memory to reserve enough space for all possible variable types.

Let’s think about how to useAnyTo wrap a variable of value type, the memory layout will be more complicated

In the era of OO languages, Parametric Polymorphism is often used to address these problems, which in Swift is generic

For example, for the example above, if we defined it using the Swift generic, it would look like this

struct Buffer<Element>{  
    let count: Int

    subscript(at: Int) ->Element {
       // fetch from storage
    }
}
Copy the code

This way, we can tell the compiler what types of variables should be included in the Buffer. Therefore, if the wrong code is written, the compiler will immediately report an error, such as

var words: Buffer<String> = ["generics"," FTW "] words[0] = 42 // error: Cannot assign value of type 'Int' to type 'String' var boxes: Buffer<CGRect> = words // error: Cannot convert value of type 'Buffer<String>' to specified type 'Buffer<CGRect>' var boxes: Buffer // error: Reference to generic type 'Buffer' requires arguments in <... >Copy the code

For generic types, we may not declare the specific type of the generic type explicitly at initialization if the compiler has enough information to derive the type

Let words: Buffer = ["generics"," FTW "] let words: Buffer<String> = ["generics"," FTW"Copy the code

This way, we can have a compact layout in memory for different variable types without wasting unnecessary memory space

Based on type derivation techniques, we can write more convenient code for generics, such as

// Optimization Opportunities let numbers: Buffer = [1,1,2,3,5,8,13] var total = 0 for I in 0.. <numbers.count { total += numbers[i] }Copy the code

However, for buffers, if you write directly to Int’s default, you will still encounter compilation errors, for example

extension Buffer { func sum() -> Element { var total = 0 for i in 0.. <self.count { // error: Cannot convert value of type 'Element' to expected argument type 'Int' total += self[i] } return total // error: Cannot convert return expression of type 'Int' to return type 'Element' } } let total = numbers.sum()Copy the code

To solve this problem, we simply add constraints to generic types

Func sum() -> Element {}} let total =. Func sum() -> Element {}} let total = numbers.sum()Copy the code

Or we can extend it a little bit and define the protocol, which is not limited to the use of ints

Func sum() -> Element {}} let total = number.sum ()Copy the code

Protocol design

In the above example, we have created a generic type, but our abstraction is not enough. If we want to extend the scope of generic adaptation a bit more, we need to define behavior with protocols. Let’s look at the familiar set types and see how do we define a reasonable set protocol

protocol Collection {  
    associatedtype Element
}

struct Buffer<Element> { }  
extension Buffer: Collection { }

struct Array<Element> { }  
extension Array: Collection { }

struct Dictionary<Key,Value> { }  
extension Dictionary: Collection {  
    typealias Element = (Key,Value)
}
Copy the code

If we want to add subscripts to a collection type, we can do this

protocol Collection { associatedtype Element var count: Int { get } subscript(at: Int) -> Element } extension Collection { func dump() { for i in 0.. <count { print(self[i]) } } }Copy the code

In fact, this design is oversimplified. Consider the Array and Dictionary scenario. For Array, finding elements by subscript is relatively easy and the implementation is obvious, but for Dictionary, we also need proper wrapping, such as

extension Dictionary: Collection {  
    private var _storage: HashBuffer<Element>
    struct Index {
        private let _offset: Int
    }
    subscript(at: Index) -> Element {
        return _storage[at._offset]
    }

    func index(after: Index ) -> Index
    var startIndex: Index
    var endIndex: Index
}
Copy the code

Based on the above considerations, we can generalize the collection protocol once more to see if the effect is better

Protocol Collection {associatedType Element associatedType Index // Note that this is not a simple Int, but the more general Index subscript(at: Index) -> Element func index(after: Index ) -> Index var startIndex: Index { get } var endIndex: Index { get } }Copy the code

Here we consider the various scenarios of subscript operation, and use a more flexible Index type to define the Index behavior corresponding to the subscript operation, which basically covers the scenarios we encounter daily, and leaves the more variable implementation method to the specific algorithm implementation code. For example

Extension Collection where Index: Equatable /* Extension Collection where Index: Equatable /* Extension Collection where Index: Equatable /* */ {var count: Int {var I = 0 var position = startIndex while position! = endIndex { position = index(after: position) i += 1 } return i } }Copy the code

Or we could be more efficient and write the constraints directly into the protocol so that we don’t have to define the constraints for each specific generic type

protocol Collection {  
    associatedtype Element
    associatedtype Index : Equatable
}

extension Dictionary.Index: Equatable { }  
Copy the code

Customization Points

Let’s consider that for the same protocol function declaration, we can have different implementation methods, such as

Extension Collection {/// The number of elements in The Collection var count: Int { var i = 0 var position = startIndex while position ! = endIndex { i += 1 position = index(after: Extension Dictionary {/// The number of elements in The collection var count: Int { return _storage.entryCount } }Copy the code

When we use count, most of the time we just want to call a simple implementation and don’t care much about performance, and sometimes we want a high-performance solution. In generics, we can introduce the concept of customization points to meet the needs of various customization scenarios

1529021676023.png” />

In the example above, the default implementation of the count function is embedded in the protocol declaration, while the better implementation of the count function will be used in the case of Dictionary, elegantly encapsulating different implementations of the same function while preserving the generic declaration.

Protocol Inheritance

Swift advocates a protocol-oriented approach to programming, so it is natural for us to transfer some of the good features of object-oriented programming into the protocol design. Consider extending the collection protocol to special features like lastIndex, Shuffle, etc. This is where inheritance is appropriate. A concrete code is expressed as follows

protocol BidirectionalCollection: Collection { func index(before idx: Index) -> Index } extension BidirectionalCollection { func lastIndex(where predicate: (Element) -> Bool) -> Index? { var position = endIndex while position ! = startIndex { position = index(before: position) if predicate(self[position]) { return position } } return nil } }Copy the code

In protocol-oriented design, it is important to remember that protocols are used to define behavior but implement different scenarios, rather than overly specialized interfaces. Let’s look at a bad design approach

Shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle shuffle */ protocol ShuffleCollection: Collection {func index(startIndex idx: Index, offsetBy: Int) -> Index func swapAt(pos idx: Index, otherPos idx: Index) } extension ShuffleCollection { mutating func shuffle() { let n = count guard n > 1 else { return } for (i, pos) in indices.dropLast().enumerated() { let otherPos = index(startIndex, offsetBy: Int.random(in: i.. <n)) swapAt(pos, otherPos) } } }Copy the code

If you think about it, for ShuffleCollection, you need two behaviors — random access to elements — and modification of internal variables to implement shuffle

Therefore, by defining finer grained protocols, appeals can exhibit more polymorphism

protocol RandomAccessCollection: BidirectionalCollection { func index(_ position: Index, offsetBy n: Int) -> Index func distance(from start: Index, to end: Index) -> Int } protocol MutableCollection: Collection { subscript (index: Index) -> Element { get set } mutating func swapAt(_: Index, _: Index) { } } extension RandomAccessCollection where Self: MutableCollection { mutating func shuffle() { let n = count guard n > 1 else { return } for (i, pos) in indices.dropLast().enumerated() { let otherPos = index(startIndex, offsetBy: Int.random(in: i.. <n)) swapAt(pos, otherPos) } } }Copy the code

Reasonable protocol design ideas, we can use the following class diagram to express

From the top down, the generalized protocols become more and more specialized, corresponding to a narrower and narrower range of applicable scenarios (however, make sure the scenarios are as broad as possible at this point), and from the bottom up, the protocols of the superclass can always abstract out more behavior. Protocol inheritance relationships should be designed more carefully than class inheritance relationships to preserve the atomicity and polymorphism of protocol behavior as much as possible, making the code easier to extend and assemble.

Conditional Conformance

Conditional consistency was introduced in Swift 4.1 to express the semantics that generic types follow a particular protocol under certain conditions, for example

// Define an Purchaseable protocol protocol {func buy()} to define a struct Book that complies with that protocol: Purchaseable {func buy() {print("You bought a book")}} // Array follows the protocol, and each element follows the protocol. Purchaseable where Element: Purchaseable {func buy() {for item in self {item.buy()}}} // The following way causes a runtime crash in Swift4.1, but in Swift4.2 the issue has been fixed let items: Any = [Book(), Book(), Book()] if let books = items as? Purchaseable { books.buy() }Copy the code

In Swift4.2, enhancements to conditional consistency enable us to do a lot more. For example, multi-protocol conditional consistency checking is also possible

extension Range: Collection, BidirectionalCollection, RandomAccessCollection where Bound: Strideable, Bound.Stride: SignedInteger {// have multiple constraints} // You can also use typeAlias to wrap constraints, which looks more convenient: Collection, BidirectionalCollection, RandomAccessCollection { // ... } typealias CountableRange<Bound: Strideable> = Range<Bound> where Bound.Stride: SignedIntegerCopy the code

How do generics and classes choose

Swift is a multi-programming paradigm language that you can use for both protocol oriented programming (POP) and object oriented programming (OOP).

If you are using object inheritance, keep in mind the Richter’s Substitution principle, which we will review in detail

One of the basic principles of object-oriented design. Richter’s substitution rule says that wherever a base class can appear, a subclass must appear. LSP is the cornerstone of inheritance reuse. The base class can be reused only when the derived class can replace the base class and the function of the software unit is not affected. The derived class can also add new behaviors on the base class. Therefore, the principle of inheritance is that inheritance must ensure that the properties held by the superclass are still true in the subclasses. That is, instances of A subclass have an IS-A relationship when they should be able to replace instances of any of their superclasses, constituting an inheritance relationship.

The simple code is shown below

class Vehicle { ... }  
class Taxi: Vehicle { ... }  
class PoliceCar: Vehicle { ... }  

extension Vehicle {  
    func drive() { ... }  
}

taxi.drive()  
Copy the code

However, using OOP in real programming often presents a dilemma: The design of superclasses is often related to subclasses. If a new subclass appears, do you modify the behavior of the superclass to override the new subclass, or do you change it to a combinatorial relationship?

When we use POP, combined with generics, we can provide polymorphism in behavior, which in some sense is a more flexible solution. In Swift, protocols can have default implementations, can add constraints, can provide queries with conditional consistency, and most importantly, can provide inheritance relationships. All of these features help us better reuse code under POP.

Under POP, the Richter substitution principle still applies to protocol inheritance, as shown in the following code

class Vehicle { ... } class Taxi: Vehicle { ... } class PoliceCar: Vehicle { ... } protocol Drivable { func drive() } extension Vehicle: Drivable {} // Extension Drivable {func sundayDrive() {if Date().isSunday {drive()}} // All subclasses also have the behavior defined by the protocol PoliceCar().sundayDrive()Copy the code

Let’s consider the factory mode implementation under POP as a textbook example to help us understand the powerful protocol-related features in Swift

// Initializer Requirements protocol Decodable {* Note: */ init(from decoder: decoder) throws {... } } extension Decodable { static func decode(from decoder: Decoder) throws -> Self { return try self.init(from: Decoder)}} Class Vehicle: Decodable {required /* not available */ init(from decoder: decoder) throws {... }} class Taxi: Vehicle {var hourlyRate: Double required /* cannot be missed */ init(from decoder: decoder) throws {... } } Taxi.decode(from: decoder) // produces a TaxiCopy the code

Finally, in Swift we can also use final to modify a class to indicate that the class cannot be inherited

final class EndOfTheLine: Decodable { init(from decoder: Decoder) { ... } // There is no need to write required, and the compiler knows that the implementation of the init method only needs to be found here, rather than through the inheritance chain.Copy the code

conclusion

This Session has covered many topics related to generics and protocols, so let’s briefly review and summarize the main points. 1. Generic design is an important feature of Swift language, which can maintain the characteristics of static typing and achieve the purpose of code reuse 2. Protocol design should follow the principle of top-down and bottom-up – top-down: protocol inheritance expresses more specialized behavior description – bottom-up: parent protocol should be able to encapsulate more abstract behavior to achieve code reuse effect 3. Inheritance design should follow the Richter substitution principle