1. Introduction
Generic code allows you to write flexible, reusable functions that can be used for any type based on the requirements you define. You can write code that is reusable, clearly intended, and abstract.
Generics are one of Swift’s most powerful features, and much of the Swift standard library is built on generic code. In fact, you may not even realize that generics have been used in language guides. For example, Swift’s Array and Dictionary types are generic collections.
You can create an array of Int values, or an array of String values, or even any other type of array that Swift can create. Similarly, you can create a dictionary that stores any value of a given type, and there is no type restriction.
2. Problems solved by generics
The following swapTwoInts(.:) is a standard non-generic function that swaps two Int values:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
Copy the code
As described in the input/output formal arguments, this function swaps the values of a and b with the input/output formal arguments.
The swapTwoInts(.:) function assigns the original value of b to a and the original value of a to b. You can call this function to swap the values of two Int variables.
var someInt = 3
Copy the code
The swapTwoInts(.:) function is useful, but only for Int values. If you want to swapTwoStrings, or Double values, you can only write more functions, such as the swapTwoStrings(.:) and swapTwoDoubles(.:) functions below:
func swapTwoStrings(_ a: inout String, _ b: inout String) {
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
Copy the code
You may have noticed that swapTwoInts(.:), swapTwoStrings(.:), and swapTwoDoubles(.:) have the same bodies. The only difference is that they accept different value types (Int, String, and Double).
It is more practical and flexible to write a function that can swap values of any type. Generic code allows you to write functions like this. (Generic versions of these functions are defined below.)
It is important that a and B of the three functions are defined to be of the same type. If a and B are of different types, their values cannot be swapped. Swift is a type-safe language that does not allow (for example) the exchange of values between a String variable and a Double variable. Attempting to do so raises a compilation error. Ps. IOS development and communication technology: welcome to join, no matter you are big or small white welcome to enter, share BAT, Ali interview questions, interview experience, discuss technology, we exchange learning and growth together
3. Generic functions
Generic functions can be used with any type. Here is a generic version of the swapTwoInts(.:) function mentioned above, called swapTwoValues(.:) :
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
Copy the code
The swapTwoValues(.:) and swapTwoInts(.:) functions have the same body. However, the first line of swapTwoValues(.:) and swapTwoInts(.:) is a little different. Here’s a comparison of the first line:
func swapTwoInts(_ a: inout Int, _ b: inout Int)
Copy the code
The generic version of the function uses a placeholder type name (here called T) instead of an actual type name (such as Int, String, or Double). The placeholder type name does not declare what T must be, but it does say that a and B must both be of the same type T, or the type represented by T. The type actually used by T is determined each time the swapTwoValues(.:) function is called.
The other difference is that the generic function name (swapTwoValues(.:)) is followed by the placeholder type name (T) wrapped in Angle brackets (). Angle brackets tell Swift that T is a placeholder type name in the swapTwoValues(.:) function definition. Because T is a placeholder, Swift does not look for a type that is really called T.
The swapTwoValues(.:) function can now be called the same way that swapTwoInts was called. In addition, you can pass two values of any type to the function, as long as the two arguments are of the same type. Each time swapTwoValues(.:) is called, the type for T is automatically inferred based on the value type of the function passed in.
In the following two examples, T is inferred to be Int and String, respectively:
var someInt = 3
Copy the code
SwapTwoValues (defined above)
The.:) function is inspired by a generic function called swap, which is part of the Swift standard library and can be used in your application. If you need to use swapTwoValues(
.:) function, can be directly provided by Swift swap(
.:) function, do not need to implement itself.
4. Type formal parameters
The placeholder type T in swapTwoValues(.:) above is an example of a type formal parameter. The type formal parameter specifies and names a placeholder type next to a pair of Angle brackets written after the function name (for example).
Click to get a complete ios profile
Once you specify a type formal parameter, you can use it to define the type of a function formal parameter (such as a and b in the swapTwoValues(.:) function), either as a function return value type, or as a type annotation in the function body. In different cases, type formal parameters are replaced with the actual type at the time the function is called. (in the swapTwoValues(.:) example above, the first call to the function replaces T with an Int, and the second call with a String.)
You can provide more formal parameter names by enclosing them in Angle brackets, separated by commas.
5. Name type formal parameters
In most cases, the names of type formal parameters are descriptive, such as Key and Value in Dictionary<Key, Value>, to inform the reader of the relationship between type formal parameters and generic types and functions used by generics. However, when the relationship between them does not make sense, it is customary to name them with a single letter, such as T, U, V, as in the swapTwoValues(.:) function above.
Type formal parameters are always named with capital camel nomenclature (such as T and MyTypeParameter) to indicate that they are placeholders for a type, not a value.
Generic types
In addition to generic functions, Swift allows you to define your own generic types. They are custom 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. A stack is an ordered collection of values, similar to an Array, but with more stringent operational restrictions than Swift’s Array type. Arrays allow you to insert and remove elements anywhere within them. However, new elements of the stack can only be added to the end of the collection (this is called pushing). Similarly, the stack only allows elements to be removed from the end of the collection (this is known as unstacking).
The UINavigationController class manages view controllers in its navigation hierarchy using the idea of a stack. You can call the UINavigationController class pushViewController(
Method: animated:) (or push) a view controller is added to the navigation stack, use popViewControllerAnimated (
The 🙂 method removes (or pops) a view controller from the navigation stack. The stack is a useful collection model when you need to manage a collection in a strict last in, first out fashion.
The following diagram shows the behavior of pushing and unpushing:
-
Now we have three values in the stack;
-
The fourth value goes to the top of the stack;
-
The stack now has four values, the most recently added one at the top;
-
The top element of the stack is removed, or “off the stack”;
-
After removing one element, there are three more elements in the stack.
Here’s how to write a non-generic version of the stack, in this case a stack of Int values:
struct IntStack {
Copy the code
This structure uses an Array property called items to store values in the stack. Stack provides two methods, push and POP, to add and remove values from the Stack. These 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. But it is more practical to define a generic Stack that can manage stacks of any type of value.
Here is a generic version of the same code:
struct Stack<Element> {
Copy the code
Note that the generic Stack is essentially the same as the non-generic version, except that it replaces the actual Int with a type-form parameter called Element. This type form parameter is written in Angle brackets (), immediately after the structure name.
Element defines a placeholder name for the “Element of a type” provided later. This future type can be referenced as “Element” anywhere within the structure definition. In this example, there are three places where Element is used as a placeholder:
-
Create a property called items and initialize it with an empty array of Element type values;
-
Specify that the push(_:) method has a formal argument called item, which must be of type Element;
-
Specifies that the return value of the pop() method is an Element value.
Because it is generic, the Stack can be used to create a Stack of any type that is valid in Swift in a similar way to Array and Dictionary.
Create a new Stack instance by writing out the type stored in the Stack in Angle brackets. For example, to create a new string Stack, write Stack() :
var stackOfStrings = Stack<String>()
Copy the code
Here’s how stackOfStrings looks after pushing four values onto the stack:
Remove from the stack and return the top value, “cuatro” :
let fromTheTop = stackOfStrings.pop()
Copy the code
This is the top value of the stack after the stack:
Extend a generic type
When you extend a generic type, you do not need to provide a list of type-form parameters in the definition of the extension. The type formal parameter list defined by the primitive type is still valid in the extension body, and the primitive type formal parameter list name is also used to extend the type formal parameter.
The following example extends the generic Stack type by adding a read-only calculation property called topItem to return the top element without removing it from the Stack:
extension Stack {
Copy the code
The topItem property returns an optional value of type Element. 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 a list of type-form parameters. Instead, the extension uses Stack’s existing type-form parameter name, Element, to specify the optional type of the evaluated property topItem.
Now, without removing the element, you can access and query the top element of any Stack instance using its topItem evaluation property:
if let topItem = stackOfStrings.topItem {
Copy the code
8. Type constraints
The swapTwoValues(.:) function and the Stack type can be used for any type. However, there are times when it is useful to enforce specific type constraints on the types used for generic functions and generic types. A type constraint indicates that a type formal parameter must inherit from a particular class or follow a particular protocol or combination protocol.
For example, Swift’s Dictionary type sets a limit on the types of keys that can be used in dictionaries. As described in the dictionary, the type of the dictionary key must be hashable. That is, it must provide a way to make it uniquely representable. Dictionary needs its keys to be hashable so that it can check if the Dictionary contains the value of a particular key. Without this requirement, the Dictionary cannot distinguish between inserting and replacing the value of a given key, nor can it look up the value of a given key in the Dictionary.
This requirement is implemented through the type constraint on Dictionary key types, which specifies that key types must follow the Hashable protocol defined in the Swift standard library. All Swift primitive types (such as String, Int, Double, and Bool) are hashable by default.
When creating custom generic types, you can define your own type constraints that provide powerful generic programming capabilities. Abstract concepts like Hashable represent types in terms of conceptual characteristics rather than exact types.
J. J. ** 8.1 **** Type constraint syntax **
Place a class or protocol after a formal parameter name as part of the formal parameter list, separated by a colon, to write out a type constraint. Here is the basic syntax for a generic function type constraint (the same syntax as for generic types) :
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
Copy the code
The hypothetical function above has two formal parameters. The first type form argument, 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 the SomeProtocol protocol.
J. J. ** 8.2 **** Application of type constraints **
This is a non-generic function called findIndex(ofString:in:) that looks for a given String value in a given array ofString values. The findIndex(ofString:in:) function returns an optional Int, which returns the index of the first matching string in the array if the given string is found, or nil if the given string is not found: findIndex(ofString:in:)
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
Copy the code
The findIndex(ofString:in:) function can be used to find a string value in an array of strings:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
Copy the code
The principle of finding an index of a value in an array applies only to strings. However, you can write the same function with a generic function by replacing all strings with a value of type T.
Here’s a function called findIndex(of:in:), which is probably a generic version of the findIndex(ofString:in:) function you’d expect. Notice that the return value of this function is still Int, right? Because the function returns an optional index number rather than an optional value in the array. This function is not compiled, for reasons explained later in the example:
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
Copy the code
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, the meaning of “equal” for that class or structure is not something Swift can guess for you. Therefore, there is no guarantee that this code will work for all types that T can represent, and you will get a corresponding error when you try to compile this code.
There is no way out. Anyway, the Swift library defines a protocol called Equatable that implements the equality operator (==) and inequality operator (! =), used to compare any two values of the type. All types in the Swift standard library automatically support the Equatable protocol.
Any type of Equatable can safely be used in the findIndex(of:in:) function because those types are guaranteed to support the equality operator. To express this fact, write the Equatable type constraint as part of the type formal parameter definition when you define a function:
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
Copy the code
The type-formal parameter for findIndex(of:in:) is written as T: Equatable, representing “any type T that follows the Equatable protocol”.
The findIndex(of:in:) function can now compile successfully and can be used on any Equatable type, such as Double or String:
Let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])Copy the code
9. Association type
When defining a protocol, it is sometimes useful to declare one or more association types in the protocol definition. An association type gives a placeholder name to the type used in the protocol. The actual type for the association type is not specified until the protocol is adopted. The association type is specified by the associatedType keyword.
J. J. ** 9.1 **** Association type applications **
Here is a sample protocol called Container that declares an association type called ItemType:
protocol Container {
Copy the code
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 get the number of elements in the container through a count attribute that returns an Int;
-
It must be possible to retrieve every element in the container by subscripting the Int index value.
This protocol does not specify how elements are stored in the container, nor what types of elements are allowed to be stored in the container. The protocol only specifies the three functions that must be provided by the type of Container to be a Container. Types that follow the protocol can provide additional functionality as long as these three requirements are met.
Any type that complies with the Container protocol must be able to specify the type of the value it stores. In particular, it must ensure that only elements of the correct type can be added to the container, and that the element type returned by the subscript of that type must be the correct type.
To define these requirements, the Container protocol needs a way to refer to the type of elements that the Container will store without knowing the specific type of the Container. The Container protocol specifies that all values passed to the append(_:) method must be of the same type as the elements in the Container, and that the subscripts returned by the Container must be of the same type as the elements in the Container.
To implement these requirements, the Container protocol declares an association type called ItemType, written associatedType ItemType. The protocol does not define what type ItemType is; this information is left to the type that follows the protocol. However, the alias ItemType, which provides a way to refer to the element types in a Container, defines a type for Container methods and subscripts, ensuring that any behavior expected by the Container is satisfied.
Here is the previous non-generic version of IntStack, making it comply with the Container protocol:
struct IntStack: Container {
Copy the code
IntStack implements all requirements of the Container protocol and encapsulates existing methods in the IntStack to meet these requirements.
In addition, IntStack specifies that the type applicable to ItemType is Int in order to implement the Container protocol. Typealias ItemType = Int Converts the Abstract ItemType type to a concrete Int.
Thanks to Swift’s type inference, you don’t really have to declare a specific Int ItemType in the IntStack definition. Because IntStack follows all the requirements of the Container protocol, Swift can infer the appropriate ItemType by simply looking at the item form argument of the append(_:) method and the return type of the subscript. If you actually remove typeAlias ItemType = Int from the above code, everything will work fine because ItemType is very clear.
You can also make a generic Stack type that follows the Container protocol:
struct Stack<Element>: Container {
Copy the code
This time, Element is used for the item argument and the return type of the subscript of the append(_:) method. Therefore, for this container, Swift can infer that Element is the type applicable to ItemType.
J. 2008 ** 9.2 **** Adding constraints to association types **
You can add constraints to an association type in a protocol to require that the type to follow meet the constraint. For example, the following code defines a version of Container that requires the elements in the Container to be decidable.
protocol Container {
Copy the code
To comply with this version of Container, the Container’s Item must comply with the Equatable protocol.
Bradley J. Bradley ** 9.3 **** Using protocol ** in association type constraints
A protocol can emerge as a requirement of its own. For example, here is a protocol that refines the Container protocol by adding a suffix(
Methods:). suffix(
The 🙂 method returns a given number of elements in the container from back to front and stores them in an instance of type Suffix.
protocol SuffixableContainer: Container {
Copy the code
In this protocol, Suffix is an association type, like the Item type of the Container in the example above. Suffix has two constraints: it must follow the SuffixableContainer protocol (which is currently defined), and its Item type must be the same as the Item type in the container. The constraint on Item is a WHERE clause, which is discussed in the extension below with the generic WHERE clause.
Here is an extension of the Stack type of loop strong reference from the closure, which adds compliance to the SuffixableContainer protocol:
extension Stack: SuffixableContainer {
Copy the code
In the example above, Suffix is the associated type of the Stack, which is the Stack, so the Suffix of the Stack returns another Stack. In addition, a type that follows SuffixableContainer can have a different Suffix type than itself — that is, a Suffix operation can return a different type. For example, here’s an extension of the non-generic IntStack type, which adds SuffixableContainer to follow, using Stack as its suffix type instead of IntStack:
extension IntStack: SuffixableContainer {
Copy the code
Bradley J. Bradley ** 9.4 **** Extends existing types to specify association types **
You can extend an existing type to conform to a protocol, as described in adding protocol compliance to the extension. This includes a protocol with an association type.
Swift’s Array type already provides the append(_:) method, the count attribute, and the subscript of its element with an Int index. These three functions meet the requirements of the Container protocol. This means that you can extend the Array to comply with the Container protocol by simply declaring the Array adoption protocol. Implemented with an empty extension, such as using an extension declaration to adopt the protocol:
extension Array: Container {}
Copy the code
The array’s existing append(_:) method and subscript enable Swift to infer an 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 Whe****re clause
As described in type constraints, type constraints allow you to define requirements on generic functions or type formal parameters associated with generic types.
Type constraints are also useful when defining requirements for associated types. This is done by defining a generic Where clause. The generic Where clause allows you to require that an association type must follow the specified protocol, or that the specified type formal parameters and the association type must be the same. A generic Where clause begins with the Where keyword, followed by a constraint on the association type or a relationship that the type and the association type agree with. The generic Where clause is written before the left brace of a type or function body.
The following example defines a generic function called allItemsMatch that checks whether two Container instances contain the same elements in the same order. The function returns true if all elements match, false otherwise.
The two containers being examined do not have to be of the same type (although they can be), but they must have the same element type. This requirement is embodied by the type constraint and the generic Where clause:
func allItemsMatch<C1: Container, C2: Container>
Copy the code
This function takes two formal arguments, someContainer and anotherContainer. The someContainer formal parameter is of type C1, and the anotherContainer formal parameter is of type C2. C1 and C2 are type-formal parameters of two container types whose types are determined when the function is called.
Here are the requirements for setting the two type formal parameters of a function:
-
C1 must comply with the Container protocol (C1: Container).
-
C2 must also comply with the Container protocol (written C2: Container);
-
C1 ItemType must be the same as C2 ItemType (c1.itemType == c2.itemType).
-
C1’s ItemType must comply with the Equatable protocol (c1. ItemType: Equatable).
The first two requirements are defined in the function’s type-form parameter list, and the last two requirements are defined in the function’s generic Where clause.
These requirements mean:
-
SomeContainer is a C1 container;
-
AnotherContainer is a C2 container.
-
The elements in someContainer and anotherContainer have the same type;
-
Elements in someContainer can be used with the inequality operator (! =) check to see if they are different.
Taken together, the last two requirements mean that elements in anotherContainer can also pass! = operator, 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 of different types.
The allItemsMatch(.:) function starts by checking whether the two containers have the same number of elements. If they have different numbers of elements, they cannot match, and the function returns false.
After checking the number, use a for-in loop and the half-open interval operator (.. <) iterate over all elements in someContainer. The function checks to see if each element in someContainer is not equal to the corresponding element in anotherContainer. If the elements are not equal, the containers do not match, and the function returns false.
If the loop completes without a mismatch, the two containers match, and the function returns true.
Here is an example of the allItemsMatch(.:) function:
var stackOfStrings = Stack<String>()
Copy the code
The above example creates a Stack instance to store String values, pushing three strings onto the Stack. We also create an Array instance that initializes the Array with three literals of the same string. Although the stack and array types are different, they both follow the Container protocol, and they contain the same value type. Therefore, you can call the allItemsMatch(.:) function with those two containers as formal arguments to the function. In the above example, the allItemsMatch(.:) function correctly reports all element matches in the two containers.
11. **** with extension of the generic Where clause
You can also use the generic WHERE clause as part of the extension. The following generic Stack structure extends the previous one by adding an isTop(_:) method.
extension Stack where Element: Equatable {
Copy the code
This new isTop(:) method first verifies that the stack is not empty and then compares the given element to the top of the stack. If you try to do this without using the generic WHERE clause, you may run into a problem: the isTop(:) implementation uses the == operator, but the definition of Stack does not require its elements to be equal, so using the == operator causes a runtime error. Using the generic where clause allows you to add a new requirement to the extension so that the extension only adds an isTop(_:) method to the stack if the element in the stack is equable.
Here’s how:
if stackOfStrings.isTop("tres") {
Copy the code
If you try to call the isTop(_:) method on a stack where the elements are unequalized, you will get a runtime error.
struct NotEquatable { }
Copy the code
You can use the generic WHERE clause to extend a protocol. The following Container protocol extension adds a startsWith(_:) method.
extension Container where Item: Equatable {
Copy the code
The startsWith(:) method first ensures that the container has at least one element, and then it checks that the first element is the same as the given element. The new startsWith(:) method can be applied to any type that follows the Container protocol, including the stacks and arrays we used earlier, as long as the elements of the Container can be evaluated, etc.
if [9, 9, 9].startsWith(42) {
Copy the code
The generic WHERE clause in the example above requires Item to follow the protocol, but you could also write a generic WHERE clause that requires Item to be of a specific type. Such as:
extension Container where Item == Double {
Copy the code
This chestnut adds the average() method to the container when Item is Double. It iterates over the elements in the container to add them up, 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 as part of an extension in a generic WHERE clause, just as you write generic WHERE clauses elsewhere. Each requirement is separated by a comma.
12. **** A. Where B. Where C. Where D. Where
When you are already in a generic type context, you can include the generic WHERE clause as part of the declaration, which has no generic type constraints of its own. For example, you can write a generic WHERE clause in a generic type subscript or in a generic type extension method. The Container structure is generic, and the where clause in the following example specifies what requirements the new methods in the Container need to meet to be available.
extension Container {
Copy the code
This example adds an average() method to the Container when the element is an integer, and it also adds the endsWith(_:) method when the element is decidable. Both of these functions contain the generic WHERE clause, which imposes type restrictions on the Item type, the formal parameter of the generic declared in the Container.
If you don’t want to use the context WHERE clause, you need to write two extensions, each using the generic WHERE clause. The following example has the same effect as the above example.
extension Container where Item == Int {
Copy the code
Using the context where clause, average() and endsWith(_:) both unload the same extension because each method’s generic where clause declares the premises it needs to satisfy to be valid. Moving these requirements to the extension’s generic WHERE clause allows the method to work in the same way, but this requires one extension for one requirement.
13. The generic Where clause of **** association type
You can include a generic WHERE clause in the association type. For example, suppose you want to make a Container that contains iterators, such as the Sequence protocol in the standard library. So you would write:
protocol Container {
Copy the code
The generic WHERE clause in Iterator requires the Iterator to iterate over all elements in the container of the same type, regardless of the type of the Iterator. The makeIterator() function provides access to the container’s traverser.
For a protocol that inherits from another protocol, you can add a qualification to the inherited protocol’s association type by including a generic WHERE clause in the protocol declaration. For example, the following code declares a ComparableContainer protocol that requires Item to comply with Comparable:
protocol ComparableContainer: Container where Item: Comparable { }
Copy the code
14. **** generic subscript
Subscripts can be generic, and they can contain generic WHERE clauses. You can write type placeholders in Angle brackets after subscript, and you can write generic WHERE clauses before subscript block curly braces. For example:
extension Container {
Copy the code
The Container protocol extension adds an array that accepts a list of indexes and returns an array containing the elements of the given index. The generic subscript is qualified as follows:
-
The generic formal parameter Indices in Angle brackets must be of a type that follows the Sequence protocol in the standard library.
-
Subscripts take a single formal parameter, indices, which is an instance of the indices type;
-
The generic WHERE clause requires that the traverser of the sequence must traverse elements of type Int. This ensures that the indexes in the sequence are of the same type as container indexes.
Taken together, these qualifications mean that the indices form parameter passed in is a sequence of integers.