Level: ★☆☆ Tag: “iOS” “Swift 5.1” “Generic” author: Muling Luo review: QiShare team


Using generics allows us to write flexible, reusable functions and types that can be used with any type we define. Using generics we can not only avoid duplicate code but also express code intent in a clearer and more abstract way.

Generics are one of Swift’s most powerful features, and many of Swift’s standard libraries are compiled using generic code.

Problems solved by generics

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}
func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}
Copy the code

These three functions swap two values of the corresponding type. But the essence of a regression function is that the bodies of these three functions are the same, except that they take an Int, a Double, and a String as their input parameters. It would be more useful and flexible to write a function based on this that can swap two values of any type.

Generic function

Func swapTwoValues<T>(_ a: inout T, _ b: inout T) {letTemp = a a = b b = temp"Qishare"
var b = "Come On"
swapTwoValues(&a, &b)
print(a,b)//! < Come On QishareCopy the code

Generic functions use placeholder types instead of the actual type names, such as Int, String, or Double. In this case, the placeholder type name is T. The placeholder type name does not specify what T is, but rather means that no matter what type T represents, a and B must have the same type T. Each time the swapTwoValues(_:_:) function is called, Swift needs to do type inference to determine the actual type to use instead of T.

Generic functions differ from non-generic functions in that they are written with Angle brackets < > following the name of the function and specifying the name of the placeholder type:

. <> is used to tell the Swift function that T is the placeholder type name for the function definition. Because T is a placeholder type, Swift does not look for the actual type of T.

Type parameters

Type parameters: Parameters that can be replaced by the actual type of the function when a generic function is called. When specified and named in Angle brackets after the generic function name, it means that the type parameter is specified, and we can use this type parameter to define the function parameter, the return value type of the function. You can also use

defines multiple type parameters.
,q…>

Named type parameter

In most cases, type parameter names are descriptive, such as Key and Value in Dictionary

and Element in Array

, which show us the relationship between type parameters and the generic type or function we are using. However, when there is no meaningful relationship between them, they are often named using single letters (such as T, U, and V).

,>

Note: Always provide camel case uppercase names for type parameters (such as T and MyTypeParameter) to indicate that they are placeholders for the type, not values.

The generic type

In addition to generic functions, Swift allows us to define our own generic types, covering classes, structures, enumerated types, and can be used with any type. Similar to dictionaries or arrays.

Next we will define a Stack structure type, named Stack, before defining the Stack type, we need to know the characteristics of the Stack structure is: first in, last out, last in, first out. 1. Define stacks that can store only certain types

struct Stack { var items = [Int]() mutating func push(_ item:Int){ items.append(item) } mutating func pop(_ item:Int) ->  Int {returnVar stack_int = Stack() stack_int.push(7) stack_int.push(3) stack_int.push(2)print(stack_int)//Stack(items: [7, 3, 2])
Copy the code

What if I need to store other types? 2. Define the generic Stack type

struct Stack<Element> {
    var items = [Element]()
    mutating func push(_ item:Element){
        items.append(item)
    }
    mutating func pop(_ item:Element) -> Element {
        returnVar stack_int = Stack<Int>() stack_int.push(7) stack_int.push(3) stack_int.push(2)print(stack_int)
var stack_string = Stack<String>()
stack_string.push("QISHARE")
print(stack_string)
Copy the code

Note: The generic type Stack has a type parameter called Element, not the actual type of Int. Element is a placeholder type defined for this generic type, and you can use Element anywhere in the structure definition to refer to the actual type for future calls.

Extensions of generic types

When extending a generic type, we do not need to provide a list of type parameters as part of the extension definition. Because the type parameters defined when a generic type is defined are still available in its extension.

extension Stack { var topItem : Element? Var stack_string = Stack<String>() stack_string.push()"QISHARE")
if let topItem = stack_string.topItem {
   print(topItem)//! < QISHARE }Copy the code

Type constraints

Generic functions and generic types can be used with any type, but there are times when you need to enforce restrictions on the types that can be used together, and that’s when you need to use type constraints. For example, Dictionary keys in Swift are constrained to comply with the Hashable protocol. Type constraints: Specify that type parameters must inherit from a particular class and adhere to a protocol or combination of protocols.

Syntax for type constraints

Syntax: When a parameter type is defined, a separate class or protocol constraint is placed after the parameter name, separated from the parameter name by a colon:. Note: Type constraints can only be classes or protocols.

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {// 'T' constraint is the type inherited from 'SomeClass', 'U' constraint is the type that complies with 'SomeProtocol'}Copy the code

Use of type constraints

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}
Copy the code

Binary operator ‘==’ cannot be applied to two ‘T’ operands The == operator is not supported by all types in Swift. For example, our custom types can only apply == or! If they implement the Equatable protocol defined by the Swift library. = to compare any two values of the type. So the correct way to write it is to add a type constraint:

func findIndex<T : Equatable>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}
Copy the code

Types of associated

When we define a protocol, it is sometimes useful to declare one or more association types as part of the protocol definition. The purpose of an association type is to provide a placeholder name for a type to be used as part of the protocol. The actual use type of the association type is not specified until the protocol is implemented. The association type is specified using the keyword associatedType.

Use of type associations

Protocol Container {associatedType Item mutating func append(_ Item: Item) var count: Int{get} subscript(I :Int)->Item{get}} struct IntStack: Container { var items = [Int]() mutating func push(_ item:Int){ items.append(item) } mutating func pop(_ item:Int) -> Int {returnItems.removelast ()} typeAlias Item = Int! < ① mutating func append(_ item: item) {//! Var count: Int {items. Count} subscript(I: Int) -> Int {items[I]}}Copy the code

Typealias Item = Int Is an implementation of the Container protocol that converts the abstract type of Item to the concrete type of Int. Based on Swift’s type inference, the append (_ 🙂 method allows you to infer the type of Item and the type of the subscript return value. When associated types are used as part of a protocol definition, the protocol can also be implemented by generic types.

struct Stack<Element> : Container {
    var items = [Element]()
    mutating func push(_ item:Element){
        items.append(item)
    }
    mutating func pop(_ item:Element) -> Element {
        returnItems.removelast ()} typeAlias Item = Element mutating func append(_ Item: Element) { push(item) } var count: Int { items.count } subscript(i: Int) -> Element { items[i] } }Copy the code

Extend an existing type to specify an association type

As we saw in the previous protocol, when a particular type has implemented the requirements of the protocol, but has not yet declared that the type complies with the protocol. The type can be extended to declare compliance with this protocol. This is also possible when an association type is defined in the protocol.

For example, the Swift Array type provides the implementation of methods and attributes in the Container protocol, which fully matches the requirements of the Container protocol. This means that we declare that the Array complies with the Container protocol through an extension to the Array, and that the implementation of the protocol requirements within the Array can infer the actual type of the protocol associated type Item.

Extension Array: Container{} // Extend an existing type to specify the association type? Success or failure. extension Array : Container{ func associateTypeOne(_:Item){} func associateTypeTwo(_:Element){} func associateTypeThree(_ : Self){}// When implementing a protocol, Self is associated with the protocol implementation type}Copy the code

Note that after we define this extension, we can use any Array as a Container? In fact, this knowledge also need to explore their own. If we have a protocol Int_Container that specifically does not use the association type

protocol Int_Container {
    mutating func append(_ item : Int)
    var count : Int{get}
    subscript(i:Int)->Int{get}
}
Copy the code
  1. Defines a function that takes a protocol type.
func testProtocolWithAssociateTypeOne (_ parameter: the Container) {/ * error: Protocol'Container' can only be used as a generic 
constraint because it has Self or associated type requirements*/
}
func testProtocolNoAssociatetype(_ parameter: Int_Container){// Compilation succeeded}Copy the code

2. Use IS or AS to check whether a certain type complies with a specific protocol

let array : [Any] = [1,"ddd", 3]ifArray is Container {/* Error :Protocol'Container' can only be used as a generic 
constraint because it has Self or associated type requirements*/
    print("Abide by this Agreement.")}else {
    print("Failure to abide by this Agreement.")}if array is Int_Container {
    print("Abide by this Agreement.")}else {
    print("Failure to abide by this Agreement.")}Copy the code

In the 1,2 examples above, a protocol with an associated type, either as a parameter type of a function or as an attribute type of an object, or as a separate type that complies with the protocol, generates an error: Protocol ‘Container’ can only be used as a generic constraint because it has Self or associated type requirements. The compiler tells us that the Container protocol has a requirement for Self or associated types, so it can only be used as a constraint on generics. Note about Self: The system library provides the Self association type for the protocol, which by default points to the type that implements the protocol.

Public protocol Equatable {static func == (LHS: Self, RHS: Self) -> Bool} Equatable {// Default association 'Self' to 'Person' static func == (LHS: Person, RHS: Person) -> Bool {return lhs.name == rhs.name
    }
    var name : String?
    var age : String?
}
Copy the code

If our Int_Container protocol definition uses the association type Self, the compiler will still report this error.

protocol Int_Container {
    mutating func append(_ item : Int)
    var count : Int{get}
    subscript(i:Int)->Int{get}
    static func testCompare(l:Self,r:Self)->Bool
}
Copy the code

Compare the association types of generics and protocols:

  • Generics: Use placeholder types to complete the implementation of generic type methods. The actual type of a generic type is specified by the user of the generic type. That is, specify the actual type when using.
  • Association type: use placeholder types to complete the definition of protocol methods. The actual type of the association type is specified by the implementer of the protocol.

An example of a generic constraint is the protocol of the associated type:

/ / (1) struct TempStruct < T: Container > {let title : String = "The protocol of the associated type acts as a constraint on the generic type: the actual type that replaces' T 'must comply with the' Container 'protocol."
    func showYourMagic(_ para : T) -> Void {
        print(para)}} //② func showYourMagic<T>(_ para: T) -> Void {print(para)
}
showYourMagic("Show the magic.")
Copy the code

Summary: Protocols with associated types can only be used as constraints on generics.

Add the constraint to the association type

You can add type constraints to the associated types in a protocol to require that the qualified types meet these constraints.

protocol Container {
    associatedtype Item : Equatable
    mutating func append(_ item : Item)
    var count : Int{get}
    subscript(i:Int)->Item{get}
}
Copy the code

Use protocols in constraints of association types

Using a protocol in a constraint of an association type can appear as part of a protocol requirement. (The current protocol can appear as a protocol requirement of the association type). SuffixableContainer is inherited from Container. It implements the following functions: To implement an instance of the protocol type, you need to create a new instance by SuffixableContainer.

// Define inheritance protocol protocol SuffixableContainer: Container {/* The newly constructed association type 'suffix' has two constraints: 1. The 'suffix' type specified when implementing this protocol must be the type 2 that implements the 'SuffixableContainer' protocol. The storage Item type 'Item' of the container type that 'suffix' occupies must be the same as the storage Item that currently implements this protocol. */ associatedtype suffix : SuffixableContainerwhereSuffix.item == Item /* 'Item' the actual type of the associated type is determined by the placeholder type of the generic type. Func suffix(_ size: Int) -> suffix} this method must ensure that the container of type String, truncated suffix, rearranged container is still String */ func suffix(_ size: Int) -> suffix} // implement extension Stack: SuffixableContainer { func suffix(_ size: Int) -> Stack { var result = Stack()for index in(count-size).. <count { result.append(self[index]) }returnVar stack_int = Stack<Int>() stack_int.push(7) stack_int.push(3) stack_int.push(2) stack_int.append(4)let suffix_int = stack_int.suffix(3)
print(stack_int,suffix_int)//3 2 4
Copy the code

SuffixableContainer in the preceding example, SuffixableContainer is used to constrain SuffixableContainer. Based on the suffix(_ 🙂 -> suffix this method must ensure that the truncated suffix of a particular container of type String is still the container of type String. Explain the constraints on the associated type suffix:

  • Specified when implementing this protocolsuffixMust be of type implementationSuffixableContainerProtocol type.
  • thissuffixPlaceholder container type storage item typeItemMust be consistent with the current store item that implements this protocol type (call type).itemThe actual type of the association type is determined by the placeholder type of the generic type.

The genericwhereclosure

The WHERE closure can require that the association type must adhere to a particular protocol, or that a particular type parameter must be equal to the association type. The WHERE closure starts with the WHERE keyword, followed by the constraint of the association type or the equality relationship between the type parameters and the association type. We can set our constraints by writing a generic WHERE clause before the curly braces of the type or function body. Illustrate the ability to match whether two containers are equal.

func twoContainerIsEqual<C1:Container,C2:Container>(_ someContainer : C1 , _ anotherContainer : C2) -> Bool where C1.Item == C2.Item , C2.Item : Equatable {
    /*whereClosures have the following constraints on association types: 1. The container element type is consistent, and 2. The element type complies with the 'Equatable' protocol */ifsomeContainer.count ! = anotherContainer.count {return false
    }
    for i in0.. <someContainer.count {ifsomeContainer[i] ! = anotherContainer[i] {return false}}return true
}
Copy the code

usewhereClosures extend generics

1. Where closures can be used as part of generic extensions.

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        returnStruct NotEquatable {} var notEquatableStack = Stack< notequatable> ()let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)

/* Error:Argument type 'NotEquatable' 
 does not conform to expected type 'Equatable'*/
notEquatableStack.isTop(notEquatableValue)
Copy the code

2. Where closures can be used as part of a protocol extension.

/* Protocols can be extended to provide implementations of methods, initializations, subscripts, and computed attributes for conforming to protocol types. This allows us to define lines for the protocol itself, rather than based on each type of protocol compliance */ Extension ContainerwhereItem: Equatable {// If the name of the function 'startsWith' is not the same as that required in 'container', then 'startsWith' adds a new method to the type that complies with this protocol. func startsWith(_ item: Item) -> Bool {return count >= 1 && self[0] == item
    }
}
Copy the code

3. The WHERE closure can require the Container protocol Item to be of a specific type.

extension Container whereItem == Double {average() -> Double {var sum = 0.0for index in0.. <count { sum += self[index] }return sum / Double(count)
    }
}
Copy the code

Association types use genericswhereClosure.

Use generic WHERE clauses on association types. For example, add an iterator to the Container protocol.

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
    
    associatedtype Iterator : IteratorProtocol whereIterator.element == Item func makeIterator() -> Iterator} struct Iterator<T> : IteratorProtocol{var stack: Stack<T> var count = 0 init(_ stack : Stack<T>) { self.stack = stack } typealias Element = T mutating func next() -> T? {let next = stack.count - 1 - count
        guard next >= 0 else {
            return nil
        }
        count += 1
        returnStruct stack <Element> : Container,Sequence {Container can only be used as a generic constraint. var items = [Element]() mutating func push(_ item:Element){ items.append(item) } mutating func pop(_ item:Element) -> Element {returnItems.removelast ()} typeAlias Item = Element mutating func append(_ Item: Element) { push(item) } var count: Int { items.count } subscript(i: Int) -> Element {items[I]} typeAlias IteratorType = Iterator<Element> func makeIterator() -> IteratorType {returnIterator.init(self)}} // call var stack_int = Stack<Int>() stack_int.push(7) stack_int.push(3) stack_int.push(2) stack_int.append(4)for item in stack_int {
    print(item)} // Output: 4 2 3 7Copy the code

Iterator: IteratorProtocol where iterator. Element == Item Requires that the Iterator iterate over elements of the same type as the container’s elements, regardless of the type of the Iterator.

Generic subscript

Subscripts can be generic or include a generic WHERE clause with the placeholder type name in Angle brackets after the subscript and the generic WHERE clause before the open curly bracket of the body of the subscript.

extension Container {
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Iterator.Element == Int {
            var result = [Item]()
            for index in indices {
                result.append(self[index])
            }
            return result
    }
}
Copy the code

Indices. Iterator.element == Int guarantees that the index in the sequence has the same type as the index used for the container. That is: means that the value passed for the index parameter is a sequence of integers.

Resources: Swift 5.1 official programming guide


To learn more about iOS and related new technologies, please follow our official account:

You can add the following xiaobian wechat, and note to join the QiShare technical exchange group, xiaobian will invite you to join the QiShare technical Exchange Group.

QiShare(Simple book) QiShare(digging gold) QiShare(Zhihu) QiShare(GitHub) QiShare(CocoaChina) QiShare(StackOverflow) QiShare(wechat public account)

Recommended articles: Swift 5.1 (20) – Protocol Swift 5.1 (19) – Extended Swift 5.1 (18) – Nested Type Swift 5.1 (17) – Type conversion and pattern matching IOS View and export project run logs, use of Flutter Platform Channel and source code analysis and development. How to do without cut map? Vector icon (iconFont) Getting started with guide DarkMode, WKWebView, Apple login must be adapted? ATaller Strange dance weekly