1. Preface
Generic code allows you to write flexible and reusable functions that can be used for any type according to the requirements you define. You can write reusable, clearly expressed and abstract code.
Genericity is one of Swift's most powerful features. Many swift standard libraries are built based on generic code. In fact, you don't even realize that generics have been used in language guides. For example, Swift's Array and Dictionary types are generic collections.
You can create a container An array of Int values, or an array containing String values, or even any other type of array that Swift can create. Similarly, you can create a dictionary that stores values of any specified type, and there are no restrictions on the type.
2. Problems solved by generics
The following swapTwoInts(:::) is a standard non generic function for exchanging two Int value:
func swapTwoInts(_ a: inout Int, _ b: inout Int) { let temporaryA = a a = b b = temporaryA }
As described in the input-output formal parameters, this function exchanges input-output formal parameters for a and b Value of b.
The swapTwoInts(:::) function gives the original value of b to a and the original value of a to b. You can call this function to exchange the values of two Int variables.
var someInt = 3 var anotherInt = 107 swapTwoInts(&someInt, &anotherInt) print("someInt is now \(someInt), and anotherInt is now \(anotherInt)") // Prints "someInt is now 107, and anotherInt is now 3"
The swapTwoInts(::) function is useful, but it can only be used for Int values. If you want to exchange two String values or two Double values, you can only write more functions, such as the following swapTwoStrings(::) and swapTwoDoubles(::) functions:
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 }
As you may have noticed, the function bodies of swapTwoInts(::), swapTwoStrings(::), and swapTwoDoubles(::) are the same. The only difference is that they receive different value types( Int, String, and Double).
It is more practical and flexible to write a function that can exchange values of any type. Generic code allows you to write such functions. (generic versions of these functions are defined below.)
Of the three functions, it is important that a and b are defined as the same type. If The types of a and b are different and their values cannot be exchanged. Swift is a type safe language that does not allow (for example) a variable of type String to exchange values with a variable of type Double. Attempting to do so will cause a compilation error.
3. Generic functions
Generic functions can be used for any type. Here is the above mentioned The generic version of the swapTwoInts(::) function is called swapTwoValues(::):
func swapTwoValues<T>(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA }
swapTwoValues(:) and The body of the swapTwoInts(::) function is the same. However, swapTwoValues(::) and The first line of swap two ints (::) is a little different. The following is a comparison of the first line:
func swapTwoInts(_ a: inout Int, _ b: inout Int) func swapTwoValues<T>(_ a: inout T, _ b: inout T)
The generic version of the function uses a placeholder type name (called t here) instead of an actual type name (such as Int, String, or Double ). The placeholder type name does not declare what t must be like, but it does say a and b b must all be the same type T, or the type represented by T. The type actually used by alternative T will be determined each time the swapTwoValues(::) function is called.
The other difference is that the generic function name (swapTwoValues(::)) is followed by a placeholder type name enclosed in angle brackets (< T >)( T ). Angle brackets tell Swift that t is a placeholder type name in the definition of the swapTwoValues(::) function. Because t is a placeholder, Swift does not look for a type that is really called t.
Now, you can call the swapTwoValues(::) function by calling swapTwoInts. In addition, you can pass two values of any type to the function as long as the types of the two arguments are consistent. Each call to swapTwoValues(:::) is used for The type of T is automatically inferred based on the value type of the incoming function.
In the following two examples, T is inferred as Int and Int, respectively String :
var someInt = 3 var anotherInt = 107 swapTwoValues(&someInt, &anotherInt) // someInt is now 107, and anotherInt is now 3 var someString = "hello" var anotherString = "world" swapTwoValues(&someString, &anotherString) // someString is now "world", and anotherString is now "hello"
The swapTwoValues(:::) function defined above is inspired by a generic function called swap. The swap function is part of the Swift standard library and can be used in your application. If you need to use the function of swapTwoValues(::) in your own code, you can directly use the swap(::) function provided by Swift without implementing it yourself.
4. Type and form parameters
In the above swapTwoValues(:::), placeholder type T is an example of type formal parameters. The type form parameter specifies and names a placeholder type next to a pair of angle brackets written after the function name (such as < T >).
Once you specify a type formal parameter, you can use it to define the type of a function formal parameter (such as the formal parameters a and b in the swapTwoValues(::) function), or use it as the return value type of the function, or as the type annotation in the function body. In different cases, type formal parameters are replaced by the actual type when the function is called. (in the swapTwoValues(::) example above, T is replaced with Int in the first call to the function, and String is replaced in the second call.)
You can provide more type formal parameters by writing multiple comma separated type formal parameter names in angle brackets.
5. Named type formal parameters
In most cases, the name of type formal parameters should be descriptive, such as Key and Value in dictionary < Key, Value >, so as to inform readers of the relationship between type formal parameters and generic types and functions used by generic types. However, when the relationship between them is meaningless, it is generally named with a single letter according to the Convention, such as T, U and V, such as T in the swapTwoValues(::) function above.
Type formal parameters are always named with a hump naming method starting with uppercase (such as T and MyTypeParameter) to indicate that they are placeholders for a type, not a value.
6. Generic type
In addition to generic functions, Swift allows you to define your own generic types. They are user-defined classes, structures and enumerations that can be used for any type, similar to Array and Dictionary.
This chapter will show you how to write a generic collection type called stack. Stack is an ordered collection of values, which is similar to Array, but has more strict operation restrictions than Swift's Array type. Arrays allow you to insert and remove elements anywhere in them. However, new elements of the stack can only be added to the end of the set (this is called stack pressing). Similarly, the stack only allows elements to be removed from the end of the collection (this is called out of the stack).
The UINavigationController class manages the view controller in its navigation hierarchy using the idea of stack. You can call the pushviewcontroller (: Animated:) method of the UINavigationController class to add (or push) a view controller to the navigation stack, and use the popViewControllerAnimated(:) method to remove (or pop) a view controller from the navigation stack. Stack is a useful collection model when you need to manage a collection in a strict "last in, first out" way.
The following figure shows the behavior of pressing and exiting the stack:

- Now there are three values in the stack;
- The fourth value is pressed to the top of the stack;
- There are now four values in the stack, and the recently added one is at the top;
- The top element of the stack is removed, or "out of the stack";
- After removing one element, there are three more elements in the stack.
Here is how to write a non generic version of the stack. This is a stack of Int values:
struct IntStack { var items = [Int]() mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } }
This structure uses an Array attribute called items to store the values in the stack. Stack provides two methods, push and pop, for adding and removing values from the stack. These two methods are marked mutating because they need to modify (or change) the items Array of the structure.
The IntStack type shown above can only be used for Int values. However, it is more practical to define a generic Stack, which can manage the Stack of values of any type.
Here is a generic version of the same code:
struct Stack<Element> { var items = [Element]() mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } }
Note that the generic Stack is essentially the same as the non generic version, except that the actual Stack is replaced by a type formal parameter called Element Int type. The formal parameters of this type are written in a pair of angle brackets (< Element >), immediately following the structure name.
Element defines a placeholder name for a type element provided later. This future type can be referenced with "element" anywhere within the structure definition. In this example, element is used as a placeholder in three places:
- Create an attribute named items, and initialize this attribute with an empty array of Element type values;
- appoint The push(:) method has a formal parameter called item, which must be of type Element;
- appoint The return value of the pop() method is a value of type Element.
Because it is generic, it can create a Stack of any type valid in Swift with Stack in a similar way to Array and Dictionary.
Create a new stack instance by writing the type stored in the stack in angle brackets. For example, create a new string stack Stack<String>() :
var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro") // the stack now contains 4 strings
This is an illustration of stackOfStrings after pressing four values into the stack:

Remove from the stack and return the top value, "cuatro":
let fromTheTop = stackOfStrings.pop() // fromTheTop is equal to "cuatro", and the stack now contains 3 strings
This is the stack diagram after the value at the top of the stack is out of the stack:

7. Extend a generic type
When you extend a generic type, you do not need to provide a type formal parameter list in the extended definition. The type formal parameter list of the original type definition is still valid in the extension body, and the name of the original type formal parameter list is also used to extend the type formal parameters.
The following example extends the generic Stack type and adds a read-only calculation attribute called topItem to it. The top element can be returned without removing it from the Stack:
extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } }
The topItem property returns an optional value of Element type. If the stack is empty, topItem returns nil; if the stack is not empty, topItem returns the last Element of the items array.
Note that this extension does not define the type formal parameter list. On the contrary, the extension uses the existing type formal parameter name of Stack, Element, to indicate the optional type of the calculation property topItem.
Now, without removing the element, you can access and query the element at the top of any Stack instance with the topItem calculation attribute:
if let topItem = stackOfStrings.topItem { print("The top item on the stack is \(topItem).") } // Prints "The top item on the stack is tres."
8. Type constraints
The swapTwoValues(::) function and Stack type can be used for any type. However, it is sometimes useful to enforce specific type constraints on types and generic types used for generic functions. Type constraints indicate that a type formal parameter must inherit from a specific class, or follow a specific protocol, composition protocol.
For example, Swift's dictionary type sets a limit on the types that can be used for keys in the dictionary. As described in the dictionary, the type of dictionary key must be hashable. That is, it must provide a way to make it uniquely represented. Dictionary needs its key to be hashable so that it can check whether the dictionary contains a specific key Value. Without this requirement, the dictionary cannot distinguish whether to insert or replace the value of a specified key, nor can it find the value of a given key in the dictionary.
This requirement is implemented through the type constraint on the Dictionary key type, which indicates that the key type must follow the Hashable protocol defined in the Swift standard library. All Swift basic types (such as String, Int, Double, and Bool) is Hashable by default.
When creating custom generic types, you can define your own type constraints, which can provide powerful generic programming capabilities. Abstract concepts such as Hashable represent types according to conceptual characteristics rather than exact types.
Η 8.1 type constraint syntax
Put a class or protocol after the formal parameter name of a type as part of the formal parameter list, separated by colons, to write out a type constraint. The following shows the basic syntax of a generic function type constraint (the same as that of a generic type):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here }
The above hypothetical function has two formal parameters. The first type formal parameter, T, has a type constraint that requires T to be a subclass of SomeClass. The second type formal parameter, U, has a type constraint that requires U to follow SomeProtocol protocol.
ⅶ 8.2 application of type constraints
This is a non generic function called findIndex(ofString:in:), which finds the given string value in the given string value array String value. The findIndex(ofString:in:) function returns an optional Int value. If the given string is found, it will return the index value of the first matching string in the array. If the given string is not found, it will return nil:
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
The findIndex(ofString:in:) function can be used to find a string value in a string array:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"] if let foundIndex = findIndex(ofString: "llama", in: strings) { print("The index of llama is \(foundIndex)") } // Prints "The index of llama is 2"
T he principle of finding an index of a value in an array can only be used for strings. However, through some Instead of all the strings used, you can write the same function with generic functions.
Here is a function called findIndex(of:in:), which may be a generic version of the findIndex(ofString:in:) function you expect. Note that the return value of the function is still Int? , Because the function returns an optional index number instead of an optional value in the array. This function is not compiled. The reason will be explained later in the example:
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
This function is not compiled as written above. The problem is equality checking, "if value == valueToFind". Not every type in Swift can be compared with the equality operator (= =). If you create your own class or structure to describe a complex data model, for example, for that class or structure, the meaning of "equality" is not something Swift can guess for you. Therefore, there is no guarantee that this code can be used for all types that T can represent. When you try to compile this code, you will be prompted with a corresponding error.
There is no way to go. In short, a protocol called equatable is defined in the Swift standard library. It is required to implement the equal operator (= =) and the unequal operator (! =) for the type of protocol to compare any two values of this type. All types in Swift standard libraries are automatically supported Equatable protocol.
Any type of Equatable can be safely used in the findIndex(of:in:) function because it is guaranteed that those types support the equality operator. To express this fact, when you define a function The Equatable type constraint is written as part of the type formal parameter definition:
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
The type formal parameter of findIndex(of:in:) is written as t: equable, indicating "any type T following the equable Protocol".
The findIndex(of:in:) function can now be compiled successfully and can be used for any type of Equatable, such as Double or String:
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25]) // doubleIndex is an optional Int with no value, because 9.3 is not in the array let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"]) // stringIndex is an optional Int containing a value of 2
9. Association type
When defining an agreement, it is sometimes useful to declare one or more association types in the agreement definition. The association type gives a placeholder name to the type used in the agreement. The actual type used for the association type is not specified until the agreement is adopted. The association type is specified through the associatedtype keyword.
9.1 application of association type
Here is an Example protocol called Container, which declares an association type called ItemType:
protocol Container { associatedtype ItemType mutating func append(_ item: ItemType) var count: Int { get } subscript(i: Int) -> ItemType { get } }
The Container protocol defines three functions that all containers must provide:
- You must be able to add new elements to the container through the append(:) method;
- You must be able to obtain the number of elements in the container through a count attribute that returns the Int value;
- It must be possible to fetch each element in the container through the subscript of the Int index value.
This protocol does not specify how elements are stored in the Container, nor does it specify the types of elements allowed to be stored in the Container. The protocol only specifies that you want to be a The type of Container must provide three functions. The type following the protocol can provide other functions as long as these three requirements are met.
Any type that follows the Container protocol must be able to specify the type of its stored value. In particular, it must ensure that only elements of the correct type can be added to the Container, and the element type returned by the type subscript must be the correct type.
In order to define these requirements, the Container protocol needs a method to reference the element type that the Container will store without knowing the specific type of the Container. The Container protocol needs to specify that all values passed to the append(:) method must be the same as the value type of the element in the Container, and the value returned by the Container subscript is also the same as the value type of the element in the Container.
To meet these requirements, the Container protocol declares a The associated type of ItemType is associatedtype ItemType. The protocol does not define the type of ItemType. This information is left to the type following the protocol. However, the alias ItemType provides a reference The method of element type in Container defines a type for Container method and subscript, which ensures that any expected behavior of Container is met.
This is the previous non generic version of IntStack, which follows the Container protocol:
struct IntStack: Container { // original IntStack implementation var items = [Int]() mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } // conformance to the Container protocol typealias ItemType = Int mutating func append(_ item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] } }
IntStack implements all the requirements of the Container protocol. In order to meet these requirements, it encapsulates the existing methods in IntStack.
In addition, in order to implement the Container protocol, IntStack specifies that the type applicable to ItemType is int. typealias ItemType = Int ItemType converts an abstract type to a concrete type Int type.
Thanks to Swift's type inference function, you don't have to declare a specific Int type ItemType in the IntStack definition. Because IntStack follows all the requirements of the Container protocol. By simply looking at the item formal parameter of the append(:) method and the return type of the subscript, swift can infer the appropriate value ItemType. If you really delete typealias ItemType = Int from the above code, everything will work normally, because the type of ItemType is very clear.
You can also create a generic Stack type that follows the Container protocol:
struct Stack<Element>: Container { // original Stack<Element> implementation var items = [Element]() mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } // conformance to the Container protocol mutating func append(_ item: Element) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Element { return items[i] } }
This time, the type formal parameter Element is used for the item formal parameter of the append(:) method and the return type of the subscript. Therefore, for this container, Swift can infer that Element is the type applicable to ItemType.
Η 9.2 adding constraints to association types
You can add constraints to the associated types in the protocol to require the types to comply with the constraints. For example, the following code defines a version of Container, which requires that the elements in the Container are decidable, etc.
protocol Container { associatedtype Item: Equatable mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
To follow this version of Container, the Item of the Container must follow the Equatable protocol.
⒌ 9.3 use protocol in association type constraints
Protocols can appear as their own requirements. For example, there is a protocol that refines the Container protocol and adds a suffix(:) method. The suffix(:) method returns a given number of elements from back to front in the Container and stores them in a In an instance of the Suffix type.
protocol SuffixableContainer: Container { associatedtype Suffix: SuffixableContainer where Suffix.Item == Item func suffix(_ size: Int) -> Suffix }
In this protocol, suffix is an association type, just like the Container in the above example The Item type is the same. Suffix has two constraints: it must follow the SuffixableContainer protocol (that is, the currently defined protocol), and its Item type must be the same as the Item type in the Container. The constraint of Item is a Where clause, which is discussed in the extension with generic Where clause below.
Here is an extension of Stack type with circular strong reference from closures, which adds compliance with the SuffixableContainer protocol:
extension Stack: SuffixableContainer { func suffix(_ size: Int) -> Stack { var result = Stack() for index in (count-size)..<count { result.append(self[index]) } return result } // Inferred that Suffix is Stack. } var stackOfInts = Stack<Int>() stackOfInts.append(10) stackOfInts.append(20) stackOfInts.append(30) let suffix = stackOfInts.suffix(2) // suffix contains 20 and 30
In the above example, Suffix is the association type of stack, that is Stack, so the Suffix operation of stack returns another Stack . In addition, a type that follows the Suffix container can have a Suffix type different from its own -- that is, the Suffix operation can return different types. For example, there is an extension of non generic IntStack type, which adds SuffixableContainer and uses Stack < int > as its Suffix type instead of IntStack:
extension IntStack: SuffixableContainer { func suffix(_ size: Int) -> Stack<Int> { var result = Stack<Int>() for index in (count-size)..<count { result.append(self[index]) } return result } // Inferred that Suffix is Stack<Int>. }
Η 9.4 extend existing types to specify association types
You can extend an existing type to follow a protocol, as described in adding a protocol compliance description to the extension. This includes a protocol with an association type.
Swift's Array type has provided the append(:) method, the count attribute, and the subscript of its element with the Int index. These three functions meet the requirements of the container protocol. This means that you can extend Array to follow the protocol by simply declaring that Array adopts the protocol Container protocol. It is implemented through an empty extension, such as using the extension declaration to adopt the protocol:
extension Array: Container {}
The existing append(:) method and subscript of the Array enable Swift to infer the appropriate type for ItemType, just like the generic Stack type above. After defining this extension, you can use any Array as a Container.
10. Generic Where clause
As described in type constraints, type constraints allow you to define requirements on type formal parameters related to generic functions or generic types.
Type constraints are also useful when defining requirements for associative types. This is achieved by defining a generic Where clause. The generic Where clause allows you to require that an association type must follow a specified protocol, or that the specified type formal parameters and association types must be the same. Generic Where clause The Where keyword starts with the constraint of the association type or the relationship between the type and the association type. generic paradigm The Where clause is written before the left brace of a type or function body.
The following example defines a generic function called allItemsMatch to check whether two Container instances contain the same elements in the same order. If all elements match, the function returns a Boolean value of true, otherwise it returns false.
The two containers being checked are not necessarily of the same type (although they can be), but their element types must be the same. This requirement is reflected by type constraints and generic Where clauses:
func allItemsMatch<C1: Container, C2: Container> (_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.ItemType == C2.ItemType, C1.ItemType: Equatable { // Check that both containers contain the same number of items. if someContainer.count != anotherContainer.count { return false } // Check each pair of items to see if they are equivalent. for i in 0..<someContainer.count { if someContainer[i] != anotherContainer[i] { return false } } // All items match, so return true. return true }
This function has two formal parameters, someContainer and othercontainer. someContainer formal parameter is C1 type, The formal parameter of othercontainer is C2 type. C1 and C2 are type formal parameters of two container types, and their types are determined when calling the function.
The following are the requirements set on the two type formal parameters of the function:
- C1 must follow the Container protocol (C1: Container);
- C2 must also follow the Container protocol (C2: Container);
- The ItemType of C1 must be the same as that of C2 (C1.ItemType == C2.ItemType);
- The ItemType of C1 must follow the Equatable protocol (C1.ItemType: Equatable).
The first two requirements are defined in the type formal parameter list of the function, and the last two requirements are defined in the generic Where clause of the function.
These requirements mean:
- someContainer is a C1 type container;
- Another container is a C2 type container;
- The element types in someContainer and othercontainer are the same;
- Elements in someContainer can check whether they are different through the inequality operator (! =).
Putting the latter two requirements together means that the elements in another container can also be passed= Operators are checked because they are of exactly the same type as the elements in someContainer.
These requirements allow the allItemsMatch(::) function to compare two containers, even if they are different types of containers.
The allItemsMatch(::) function starts by checking whether the number of elements in the two containers is the same. If their number of elements is different, they cannot match, and the function returns false.
After checking the quantity, traverse all the elements in someContainer with a for in loop and a half open interval operator (.. <). The function checks Whether each element in someContainer is not equal to the corresponding element in another container. If the two elements are not equal, the two containers do not match, and the function returns false.
If there is no mismatch after the loop, and the two containers are matched, the function returns true.
This is an example of the use of the allItemsMatch(::) function:
var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") var arrayOfStrings = ["uno", "dos", "tres"] if allItemsMatch(stackOfStrings, arrayOfStrings) { print("All items match.") } else { print("Not all items match.") } // Prints "All items match."
The above example creates a Stack instance to store String value, pressing three strings into the Stack. An Array instance is also created to initialize the Array with three literals of the same string. Although the types of Stack and Array are different, they all follow the Container protocol, and they contain the same value types. Therefore, you can call the allItemsMatch(::) function and use the two containers as the formal parameters of the function. In the above example, The allItemsMatch(::) function correctly reports all element matches in both containers.
11. Extension with generic Where clause
You can also use the generic where clause as part of the extension. The following generic Stack structure extends the previous chestnut and adds an isTop(:) method.
extension Stack where Element: Equatable { func isTop(_ item: Element) -> Bool { guard let topItem = items.last else { return false } return topItem == item } }
This new isTop(:) method first verifies that the Stack is not empty, and then compares the given element with the top element of the Stack. If you try to do this without using generic where clauses, you may encounter a problem: the implementation of isTop(:) needs to use the = = operator, but the definition of Stack does not need its elements to be equal, so using the = = operator will cause a runtime error. Using generic where clauses allows you to add a new requirement to the extension, so that the extension will only add isTop(:) methods to the Stack when the elements in the Stack are decidable.
This is the usage:
if stackOfStrings.isTop("tres") { print("Top element is tres.") } else { print("Top element is something else.") } // Prints "Top element is tres."
If you try to call the isTop(:) method on a stack where the element cannot wait, you will start a runtime error.
struct NotEquatable { } var notEquatableStack = Stack<NotEquatable>() let notEquatableValue = NotEquatable() notEquatableStack.push(notEquatableValue) notEquatableStack.isTop(notEquatableValue) // Error
You can use generic where clauses to extend to a protocol. The following chestnut adds one to the previous Container protocol extension startsWith() method.
extension Container where Item: Equatable { func startsWith(_ item: Item) -> Bool { return count >= 1 && self[0] == item } }
The startsWith(:) method first ensures that the Container has at least one element, and then it checks whether the first element is the same as the given element. This new The startsWith(:) method can be applied to any type that follows the Container protocol, including the stack and array we used earlier, as long as the elements of the Container can be judged.
if [9, 9, 9].startsWith(42) { print("Starts with 42.") } else { print("Starts with something else.") } // Prints "Starts with something else."
The generic where clause in the above example requires Item to follow the protocol, but you can also write a generic where clause to require Item to be of a specific type. For example:
extension Container where Item == Double { func average() -> Double { var sum = 0.0 for index in 0..<count { sum += self[index] } return sum / Double(count) } } print([1260.0, 1200.0, 98.6, 37.0].average()) // Prints "648.9"
This chestnut adds an average() method to the container when the Item is Double. It iterates through the elements in the container to add them, and then divides by the total number of containers to calculate the average. It explicitly converts the total from Int to Double to allow floating-point division.
You can include multiple requirements in a generic where clause as part of the extension, just as you write generic where clauses elsewhere. Each requirement is separated by a comma.
12. Context Where clause
When you are already in the context of a stereotype type, you can take the stereotype where clause as part of the declaration, which has no stereotype type constraints of its own. For example, you can write a canonical where clause in the subscript script of a canonical type or in the method of extending a canonical type. The Container structure is a model. The where clause in the following example specifies what requirements the new method in the Container needs to meet before it can be used.
extension Container { func average() -> Double where Item == Int { var sum = 0.0 for index in 0..<count { sum += Double(self[index]) } return sum / Double(count) } func endsWith(_ item: Item) -> Bool where Item: Equatable { return count >= 1 && self[count-1] == item } } let numbers = [1260, 1200, 98, 37] print(numbers.average()) // Prints "648.75" print(numbers.endsWith(37)) // Prints "true"
This example adds an average() method to the Container when the element is an integer. It also adds an average() method when the element is decidable endsWith() method. Both functions contain a generic where clause, which adds type restrictions to the formal parameter Item type originally declared in the Container.
If you don't want to use a contextual where clause, you need to write two extensions, each using a generic where clause. The following example has the same effect as the above example.
extension Container where Item == Int { func average() -> Double { var sum = 0.0 for index in 0..<count { sum += Double(self[index]) } return sum / Double(count) } } extension Container where Item: Equatable { func endsWith(_ item: Item) -> Bool { return count >= 1 && self[count-1] == item } }
The context where clause is used. Both average() and endsWith(:) are unloaded from the same extension, because the generic where clause of each method declares the preconditions to be met for its effectiveness. Moving these requirements to the extended paradigm where clause can make the method take effect in the same situation, but this requires that an extension corresponds to a requirement.
13. Generic Where clause of relevance type
You can include a generic where clause in the association type. For example, suppose you want to make a Container containing an ergodic, such as the Sequence protocol in the standard library. Then you would write:
protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } associatedtype Iterator: IteratorProtocol where Iterator.Element == Item func makeIterator() -> Iterator }
The generic where clause in Iterator requires the Iterator to traverse all elements in the container with the same type, no matter what type the Iterator is. The makeIterator() function provides access to the container's Iterator.
For a protocol that inherits from other protocols, you can add a qualification to the association type in the inherited protocol by including a generic where clause in the protocol declaration. For example, the following code declares a Comparable container protocol, which requires items to comply with Comparable:
protocol ComparableContainer: Container where Item: Comparable { }
14. Generic subscript
Subscripts can be generic, and they can contain generic where clauses. You can write type placeholders with angle brackets after subscript, and you can write generic where clauses before subscript code block curly brackets. For example:
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 } }
this An extension of the Container protocol adds an array that receives a series of indexes and returns an array containing a given index element. This generic subscript has the following qualifications:
- The generic formal parameter Indices in angle brackets must be a type that follows the Sequence protocol in the standard library;
- Subscript receives a single formal parameter, indices, which is an instance of indices type;
- generic paradigm The where clause requires that the iterator of the sequence must traverse elements of type Int. This ensures that the indexes in the sequence are of the same type as the container indexes.
Taken together, these qualifications mean that the incoming indexes formal parameter is a sequence of integers.