Swift Generics

Swift provides generics so that you can write flexible and reusable functions and types.

The Swift standard library is built with generic code.

Swift's Array and Dictionary types are generic collections.

You can create an Int array, a String array, or even an array of any other Swift type data.

The following example is a non-generic function exchange used to swap two Int values:

Example

// Define a function to swap two variables func swapTwoInts(_ a: inout Int, _ b: inout Int) { let temporaryA = a a = b b = temporaryA } var numb1 = 100 var numb2 = 200 print("Before swapping:\(numb1) and\(numb2)") swapTwoInts(&numb1, &numb2) print("After swapping:\(numb1) and\(numb2)")

The above program execution output is:

交换前数据: 100 和 200
交换后数据: 200 和 100

The above example only applies to swapping variables of the integer Int type. If you want to swap two String values or Double values, you have to write a corresponding function again, such as swapTwoStrings(_:_:) and swapTwoDoubles(_:_:), as shown below:

String and Double Value Swap 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 }

From the above code, their functional code is the same, only the types are different. In this case, we can use generics to avoid writing repetitive code.

Generics use a placeholder type name (represented here by the letter T) to replace the actual type name (such as Int, String, or Double).

func swapTwoValues<T>(_ a: inout T, _ b: inout T)

swapTwoValues is followed by the placeholder type name (T), enclosed in angle brackets (<T>). These angle brackets tell Swift that T is a placeholder type name within the definition of the swapTwoValues(_:_:) function, so Swift will not look for an actual type named T.

The following example is a generic function exchange used to swap two Int and String values:

Example

// Define a function to swap two variables func swapTwoValues<T>(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA } var numb1 = 100 var numb2 = 200 print("Before swapping:\(numb1) and\(numb2)") swapTwoValues(&numb1, &numb2) print("After swapping:\(numb1) and\(numb2)") var str1 = "A" var str2 = "B" print("Before swapping:\(str1) and\(str2)") swapTwoValues(&str1, &str2) print("After swapping:\(str1) and\(str2)")

The above program execution output is:

交换前数据:  100 和 200
交换后数据: 200 和 100
交换前数据:  A 和 B
交换后数据: B 和 A

Generic Types

Swift allows you to define your own generic types.

Custom classes, structures, and enums work with any type, just like the usage of Array and Dictionary.

Next, we will write a generic collection type named Stack (stack). A stack only allows new elements to be added at the end of the collection (called pushing), and elements can only be removed from the end as well (called popping).

The image is explained from left to right as follows:

  • Three values are in the stack.
  • A fourth value is pushed onto the top of the stack.
  • Now there are four values in the stack, with the most recently pushed value at the top.
  • The value at the top of the stack is removed, or popped.
  • After removing one value, the stack now has only three values again.

The following example is a non-generic version of the stack, taking an Int stack as an example:

Int Stack

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 property named items in the stack to store values. Stack provides two methods: push(_:) and pop(), used to push values into the stack and remove values from the stack. These methods are marked as mutating because they need to modify the structure's items array.

The IntStack structure above can only be used for the Int type. However, you can define a generic Stack structure so that it can handle values of any type.

Below is the generic version of the same code:

Generic Stack

struct Stack<Element> { var items = [Element]() mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } } var stackOfStrings = Stack<String>() print("String element pushed onto the stack:") stackOfStrings.push("google") stackOfStrings.push("example") print(stackOfStrings.items); let deletetos = stackOfStrings.pop() print("Popped element:" + deletetos) var stackOfInts = Stack<Int>() print("Integer element pushed onto the stack:") stackOfInts.push(1) stackOfInts.push(2) print(stackOfInts.items);

The instance execution result is:

字符串元素入栈: 
["google", "example"]
出栈元素: example
整数元素入栈: 
[1, 2]

Stack is basically the same as IntStack, with the placeholder type parameter Element replacing the actual Int type.

In the above example, Element is used as a placeholder in the following three places:

  • Creatingitemsproperty, usingElementan empty array of the type to initialize it.
  • Specifypush(_:)the only parameter of the methoditemmust be of typeElementtype.
  • Specifypop()the return value type of the method must beElementtype.

Extending a Generic Type

When you extend a generic type (using the extension keyword), you do not need to provide a type parameter list in the extension definition. More conveniently, the type parameter list declared in the original type definition is available in the extension, and the parameter names from the original type are used as references to the type parameters in the original definition.

The following example extends the generic type Stack, adding a read-only computed property named topItem, which returns the element at the top of the current stack without removing it from the stack:

Generics

struct Stack<Element> { var items = [Element]() mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } } extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } } var stackOfStrings = Stack<String>() print("String element pushed onto the stack:") stackOfStrings.push("google") stackOfStrings.push("example") if let topItem = stackOfStrings.topItem { print("The top element in the stack is:\(topItem).") } print(stackOfStrings.items)

In the example, the topItem property returns an optional value of type Element. When the stack is empty, topItem returns nil; when the stack is not empty, topItem returns the last element in the items array.

The above program execution output is:

字符串元素入栈: 
栈中的顶部元素是:example.
["google", "example"]

We can also specify an associated type by extending an existing type.

For example, Swift's Array type already provides an append(_:) method, a count property, and a subscript that accepts an Int index value to retrieve its elements. These three features all meet the requirements of the Container protocol, so you can extend Array simply by declaring that Array adopts the protocol.

The following example only creates an empty extension:

extension Array: Container {}

Type Constraints

Type constraints specify that a type parameter must inherit from a specified class, or conform to a particular protocol or protocol composition.

Type Constraint Syntax

You can write a type constraint after a type parameter name, separated by a colon, as part of the type parameter chain. The basic syntax for such type constraints on generic functions is shown below (same as the syntax for generic types):

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // 这里是泛型函数的函数体部分
}

This function has two type parameters. The first type parameter, T, has a type constraint requiring T to be a subclass of SomeClass; the second type parameter, U, has a type constraint requiring U to conform to the SomeProtocol protocol.

Example

Generics

// Non-generic function, find the index of a specified string in an array func findIndex(ofString valueToFind: String, in array: [String]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { // Return the index value if found return index } } return nil } let strings = ["google", "weibo", "taobao", "example", "facebook"] if let foundIndex = findIndex(ofString: "example", in: strings) { print("The index of example is\(foundIndex)") }

The index subscript starts from 0.

The above program execution output is:

example 的索引为 3

Associated Types

In Swift, the associatedtype keyword is used to set up associated type instances.

The following example defines a Container protocol, which defines an associated type ItemType.

The Container protocol only specifies three features that any type conforming to the Container protocol must provide. A type that conforms to the protocol can also provide additional features while satisfying these three conditions.

// Container 协议
protocol Container {
    associatedtype ItemType
    // 添加一个新元素到容器里
    mutating func append(_ item: ItemType)
    // 获取容器中元素的数
    var count: Int { get }
    // 通过索引值类型为 Int 的下标检索到容器中的每一个元素
    subscript(i: Int) -> ItemType { get }
}

// Stack 结构体遵从 Container 协议
struct Stack<Element>: Container {
    // Stack<Element> 的原始实现部分
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // Container 协议的实现部分
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

var tos = Stack<String>()
tos.push("google")
tos.push("example")
tos.push("taobao")
// 元素列表
print(tos.items)
// 元素个数
print( tos.count)

The above program execution output is:

["google", "example", "taobao"]
3

Where Clauses

Type constraints can ensure that a type conforms to the constraints defined by a generic function or class.

You can define constraints on parameters in the parameter list through a where clause.

You can write a where statement immediately after the type parameter list, followed by one or more constraints on associated types and/or one or more equality relationships between types and associated types.

Example

The following example defines a generic function named allItemsMatch, used to check whether two Container instances contain the same elements in the same order.

If all elements match, it returns true; otherwise, it returns false.

Generics

// Container protocol protocol Container { associatedtype ItemType // Add a new element to the container mutating func append(_ item: ItemType) // Get the number of elements in the container var count: Int { get } // Retrieve each element in the container using a subscript with an Int index type subscript(i: Int) -> ItemType { get } } // // Generic TOS type conforming to the Container protocol struct Stack<Element>: Container { // Original implementation part of Stack<Element> var items = [Element]() mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } // Implementation part of the Container protocol mutating func append(_ item: Element) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Element { return items[i] } } // Extension to use Array as a Container extension Array: Container {} func allItemsMatch<C1: Container, C2: Container> (_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.ItemType == C2.ItemType, C1.ItemType: Equatable { // Check that two containers contain the same number of elements if someContainer.count != anotherContainer.count { return false } // Check whether each pair of elements is equal for i in 0..<someContainer.count { if someContainer[i] != anotherContainer[i] { return false } } // All elements match, return true return true } var tos = Stack<String>() tos.push("google") tos.push("example") tos.push("taobao") var aos = ["google", "example", "taobao"] if allItemsMatch(tos, aos) { print("All elements match") } else { print("Elements do not match") }

The output of the above program execution is:

Match all elements
Other Extensions