Swift Generics
Swift provides generics to allow you to write flexible and reusable functions and types.
The Swift standard library is built using generic code.
Swift's array and dictionary types are both generic collections.
You can create an array of Int
, an array of String
, or even an array of any other Swift type.
The following example is a non-generic function called exchange
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 output of the above program is:
Before swapping: 100 and 200
After swapping: 200 and 100
The above example only works for swapping variables of type Int
. If you want to swap two String
values or Double
values, you need to write corresponding functions, 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, the functional code is identical, only the types differ. We can use generics to avoid repetitive code.
Generics use placeholder type names (denoted here by the letter T) instead of actual type names (such as Int
, String
, or Double
).
func swapTwoValues<T>(_ a: inout T, _ b: inout T)
swapTwoValues
is followed by a placeholder type name (T) enclosed in angle brackets (<T>
). These angle brackets tell Swift that T is a placeholder type name within the swapTwoValues(_:_:)
function definition, so Swift will not look for an actual type named T.
The following example is a generic function called exchange
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 output of the above program is:
Before swapping: 100 and 200
After swapping: 200 and 100
Before swapping: A and B
After swapping: B and A
Generic Types
Swift allows you to define your own generic types.
Custom classes, structs, and enums can be used with any type, similar to how Array
and Dictionary
are used.
Next, we will write a generic collection type called Stack
, which only allows adding new elements at the end (push) and removing elements from the end (pop).
The image from left to right is interpreted 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 on top.
- The top value of the stack is removed, or popped.
- After removing a value, the stack now has three values again.
The following example is a non-generic version of a stack, using 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()
}
}
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
This struct uses an Array property named items
to store values in the stack. The Stack
provides two methods: push(_:)
and pop()
, to push values onto the stack and remove values from the stack, respectively. These methods are marked as mutating
because they need to modify the struct's items
array.
The IntStack
struct above can only be used with Int
types. However, you can define a generic Stack
struct that can handle any type of value.
Here 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("Pushing string elements onto the stack: ")
stackOfStrings.push("google")
stackOfStrings.push("tutorialpro")
print(stackOfStrings.items);
let deletedToS = stackOfStrings.pop()
print("Popped element: " + deletedToS)
var stackOfInts = Stack<Int>()
print("Pushing integer elements onto the stack: ")
stackOfInts.push(1)
stackOfInts.push(2)
print(stackOfInts.items);
The example execution result is:
Pushing string elements onto the stack:
["google", "tutorialpro"]
Popped element: tutorialpro
Pushing integer elements onto the stack:
[1, 2]
The Stack
is essentially 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:
Creating the
items
property, initializing it with an empty array of typeElement
.Specifying that the
push(_:)
method's single parameteritem
must be of typeElement
.Specifying that the
pop()
method's return type must beElement
.
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's definition. More conveniently, the type parameter list declared in the original type definition is available in the extension, and these parameters from the original type are used as references to the type parameters in the original definition.
Generic
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("Pushing string elements onto the stack: ")
stackOfStrings.push("google")
stackOfStrings.push("tutorialpro")
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 output of the above program is:
Pushing string elements onto the stack:
The top element in the stack is: tutorialpro.
["google", "tutorialpro"]
We can also extend an existing type to specify an associated type. For example, Swift's Array type already provides an append(_:) method, a count property, and a subscript that takes an Int index to retrieve its elements. These three features meet the requirements of the Container protocol, so you can extend Array by simply declaring that Array adopts this protocol.
Here is an example that creates an empty extension:
extension Array: Container {}
Type Constraints
Type constraints specify that a type parameter must inherit from a specific class or conform to a particular protocol or protocol composition.
Type Constraint Syntax
You can write a type constraint after a type parameter's name, separated by a colon, as part of the type parameter list. The basic syntax for type constraints on a generic function is shown below (the same syntax applies to generic types):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
This function has two type parameters. The first type parameter T has a type constraint that requires T to be a subclass of SomeClass. The second type parameter U has a type constraint that requires U to conform to the SomeProtocol protocol.
Example
Generics
// Non-generic function to 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 if found
return index
}
}
return nil
}
let strings = ["google", "weibo", "taobao", "tutorialpro", "facebook"]
if let foundIndex = findIndex(ofString: "tutorialpro", in: strings) {
print("tutorialpro's index is \(foundIndex)")
}
Indices start at 0.
The output of the above program is:
tutorialpro's index is 3
Associated Types
In Swift, you use the associatedtype keyword to set an associated type.
Here's an example that defines a Container protocol, which declares an associated type called ItemType.
The Container protocol specifies three requirements that any type conforming to the protocol must satisfy. Conforming types can also provide additional functionality beyond these three requirements.
// Container protocol
protocol Container {
associatedtype ItemType
// Add a new item to the container
mutating func append(_ item: ItemType)
// Get the count of items in the container
var count: Int { get }
// Retrieve each item in the container via an Int index
subscript(i: Int) -> ItemType { get }
}
// Stack struct conforming to the Container protocol
struct Stack<Element>: Container {
// Original implementation of Stack<Element>
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// Implementation 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]
}
}
var tos = Stack<String>()
tos.push("google")
tos.push("tutorialpro")
tos.push("taobao")
// List of items
print(tos.items)
// Number of elements
print(tos.count)
The output of the above program is:
["google", "tutorialpro", "taobao"]
3
Where Clause
Type constraints ensure that the types meet the requirements of generic functions or classes.
You can define constraints for parameters using a where clause in the parameter list.
A where clause can be written right after the type parameter list, followed by one or more constraints for associated types, and/or one or more equality relationships between types and associated types.
Example
The following example defines a generic function called allItemsMatch
, which checks if two Container
instances contain the same elements in the same order.
It returns true
if all elements match, otherwise it returns false
.
Generics
// Container protocol
protocol Container {
associatedtype ItemType
// Add a new item to the container
mutating func append(_ item: ItemType)
// Get the number of items in the container
var count: Int { get }
// Retrieve each item in the container by subscript with Int type index
subscript(i: Int) -> ItemType { get }
}
// Generic TOS type conforming to the Container protocol
struct Stack<Element>: Container {
// Original implementation of Stack<Element>
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// Implementation 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 treat 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 if both containers have the same number of elements
if someContainer.count != anotherContainer.count {
return false
}
// Check if each pair of elements are 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("tutorialpro")
tos.push("taobao")
var aos = ["google", "tutorialpro", "taobao"]
if allItemsMatch(tos, aos) {
print("All elements match")
} else {
print("Elements do not match")
}
The output of the above program is:
All elements match