首页 Swift中的泛型
文章
取消

Swift中的泛型

什么是泛型?

泛型是一种让代码更加灵活、可重用和类型安全的编程技术。通过泛型,我们可以定义出适用于任意类型的函数、方法、类、枚举和结构体等,从而避免了冗余代码和类型错误。

泛型的基础知识

泛型函数

泛型函数是一种参数化函数,在函数定义中使用占位符类型名称。这些占位符类型名称被称为类型参数,可以在函数调用时替换成具体类型。以下示例演示了如何编写一个简单的泛型函数:

1
2
3
4
5
6
7
8
9
10
11
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var x = 1
var y = 2
swapTwoValues(&x, &y)
print("x is now \(x), and y is now \(y)")
// Output: "x is now 2, and y is now 1"

在上面的示例中,swapTwoValues函数接受两个输入参数并将它们交换。该函数使用类型参数T来表示输入参数的类型。由于Swift类型推断机制,我们在调用函数时不必指定类型参数:

1
2
3
4
5
var a = "hello"
var b = "world"
swapTwoValues(&a, &b)
print("a is now \(a), and b is now \(b)")
// Output: "a is now world, and b is now hello"

在这个示例中,我们通过调用swapTwoValues函数交换了字符串变量ab的值。

泛型类型

泛型类型允许我们编写可重用的类型,支持多种类型。类、结构体和枚举都可以是泛型类型。以下示例演示了如何编写一个简单的泛型栈类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Stack<Element> {
    var items = [Element]()
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
}

var stackOfStrings = Stack<String>()
stackOfStrings.push("one")
stackOfStrings.push("two")
stackOfStrings.push("three")

print(stackOfStrings.pop())
// Output: Optional("three")

在上述示例中,我们定义了一个泛型类型Stack<Element>Stack类型拥有一个数组items,存储泛型元素Element的值。pushpop方法允许我们向栈中添加和移除元素。

当我们创建一个Stack实例时,需要指定要存储的数据类型。在此示例中,我们创建了一个Stack<String>实例,表示该栈将存储字符串类型的数据。

泛型约束

泛型约束允许我们限制可以用作类型参数的类型。我们可以使用where子句在类型参数列表的末尾指定约束条件。以下示例演示了如何编写带有约束的泛型函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let arrayOfInts = [1, 2, 3, 4, 5]
if let index = findIndex(of: 3, in: arrayOfInts) {
    print("Index of 3 is \(index)")
} else {
    print("Value not found.")
}
// Output: "Index of 3 is 2"

在上述示例中,我们定义了一个泛型函数findIndex,该函数将查找给定值在数组中的索引。我们将类型参数T的约束类型指定为Equatable,这意味着该函数只能用于所有实现了Equatable协议的类型。

在下面的示例中,我们尝试使用findIndex函数查找一个字符串值的索引:

1
2
3
4
5
6
7
let arrayOfStrings = ["apple", "banana", "orange"]
if let index = findIndex(of: "banana", in: arrayOfStrings) {
    print("Index of 'banana' is \(index)")
} else {
    print("Value not found.")
}
// Output: "Index of 'banana' is 1"

由于字符串类型已经实现了Equatable协议,因此我们可以将该类型作为函数调用的类型参数。

关联类型

关联类型允许我们将类型参数用作协议中的关联类型。以下示例演示了如何定义一个带有关联类型的协议和实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
protocol Container {
    associatedtype Item
    
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

struct Stack<Element>: Container {
    var items = [Element]()
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
    
    // Container protocol conformance
    typealias Item = Element
    
    mutating func append(_ item: Item) {
        self.push(item)
    }
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> Item {
        return items[i]
    }
}

var stackOfInts = Stack<Int>()
stackOfInts.append(1)
stackOfInts.append(2)

print(stackOfInts[0])
// Output: 1

在上述示例中,我们定义了一个带有关联类型Item的协议ContainerStack结构体实现了该协议并提供了必要的方法和属性。

注意,我们在Stack结构体中定义了一个类型别名Item = Element来满足Container协议的要求。这样,我们就可以在Stack结构体中使用Item来代表Element类型。

泛型的高级用法

泛型扩展

泛型扩展允许我们向泛型类型添加新功能。以下示例演示了如何编写一个带有泛型扩展的Container协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension Container {
    mutating func shuffle() {
        for i in 0..<(count - 1) {
            let j = Int.random(in: i..<count)
            swapAt(i, j)
        }
    }
}

var stackOfStrings = Stack<String>()
stackOfStrings.append("one")
stackOfStrings.append("two")
stackOfStrings.append("three")

print(stackOfStrings.items)
stackOfStrings.shuffle()
print(stackOfStrings.items)

在上述示例中,我们定义了一个泛型扩展来增加Container协议的新方法shuffle。该方法使用swapAt函数随机重排了容器中的元素。

注意,我们并没有在Stack结构体中实现shuffle方法,但是我们仍然可以对其进行调用,因为Stack结构体遵循了Container协议,并从泛型扩展中继承了该方法。

类型约束和关联类型

我们可以在泛型类型或函数中使用多个类型参数,并且可以在类型参数之间添加约束条件。以下示例演示了如何使用类型约束和关联类型来定义一个泛型函数,该函数接受两个容器类型参数,并返回它们组合后的新容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func combineContainers<C1: Container, C2: Container>(_ container1: C1, _ container2: C2) -> Stack<C1.Item> where C1.Item == C2.Item {
    var result = Stack<C1.Item>()
    for i in 0..<container1.count {
        result.append(container1[i])
    }
    for i in 0..<container2.count {
        result.append(container2[i])
    }
    return result
}

var stackOfInts = Stack<Int>() 
stackOfInts.append(1) 
stackOfInts.append(2)

var stackOfDoubles = Stack<Double>() 
stackOfDoubles.append(3.14) 
stackOfDoubles.append(2.71)

let combinedStack = combineContainers(stackOfInts, stackOfDoubles)
print(combinedStack.items)

在上述示例中,我们定义了一个泛型函数combineContainers,它接受两个容器类型参数,并返回一个新的Stack容器,其中包含两个输入容器中的元素。该函数使用类型约束来要求输入容器具有相同的关联类型Item。由于Stack结构体遵循了Container协议并定义了Item类型别名,因此我们可以使用Stack<C1.Item>来表示返回的新容器类型。

泛型下标

泛型下标允许我们通过下标语法来访问泛型容器中的元素。以下示例演示了如何定义一个带有泛型下标的Container协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protocol Container {
    associatedtype Item
    
    mutating func append(_ item: Item)
    var count: Int { get }
    
    subscript(i: Int) -> Item { get set }
    
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Element == Int { get }
}

extension Container {
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Element == Int {
        var result = [Item]()
        for index in indices {
            assert(index >= 0 && index < count, "Index out of range")
            result.append(self[index])
        }
        return result
    }
}

在上述示例中,我们扩展了Container协议以添加一个带有泛型下标的方法。该方法接受一个整数序列作为输入,并返回容器中对应索引的元素数组。该方法使用类型约束来要求输入序列中的元素必须为整数类型。

注意,在扩展中实现泛型下标时,我们需要在协议中定义一个非泛型的下标,以便编译器正确地推断出泛型下标的类型。

泛型 where 语句

泛型 where 语句(Generic Where Clauses)提供了一种条件限制泛型类型和泛型函数中使用的类型。它可以在类型约束的基础上增加更多的条件限制,从而使泛型更加具体化。

泛型 where 语句语法

泛型 where 语句使用 where 关键字来引出一个或多个条件表达式。这些条件表达式使用逗号分隔,可以包含各种逻辑运算符。

1
2
3
func someFunction<T, U>(someT: T, someU: U) where T: SomeProtocol, U: SomeOtherProtocol {
    // ...
}

在这个例子中,我们使用了 where 关键字来引出两个条件表达式,这些表达式分别对泛型类型 T 和 U 进行了约束。

泛型 where 语句实践

例如,下面这个泛型函数使用了泛型 where 语句来要求数组中的元素类型必须是 Equatable 类型,并且该元素必须没有被重复添加过:

1
2
3
4
5
6
7
8
9
func uniqueElements<T>(_ array: [T]) -> [T] where T: Equatable {
    var result = [T]()
    for item in array {
        if !result.contains(item) {
            result.append(item)
        }
    }
    return result
}

在这个函数中,我们使用了泛型约束要求数组中的元素类型必须是 Equatable 类型,以便能够使用 contains(_:) 方法来判断元素是否已经存在于结果数组中。同时,由于我们想要得到唯一的元素,因此我们使用了泛型约束要求元素不能重复。

例如,我们可以使用该函数来去除一个整数数组中的重复元素:

1
2
3
let arr = [1, 2, 3, 2, 4, 3, 5]
let uniqueArr = uniqueElements(arr)
print(uniqueArr) // 输出 "[1, 2, 3, 4, 5]"

类型擦除

类型擦除(Type Erasure)是一种设计模式,它可以隐藏具体类型的实现细节,以便在不知道类型的情况下使用该类型。在 Swift 中,我们可以通过协议和泛型来实现类型擦除。

例如,下面这个示例定义了一个 AnyCollection 类型,它可以存储任意类型的集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct AnyCollection<T>: Collection {
    private let _startIndex: () -> Index
    private let _endIndex: () -> Index
    private let _index: (Index, Int) -> Index
    private let _subscript: (Index) -> T
    
    init<C: Collection>(_ base: C) where C.Element == T {
        _startIndex = { base.startIndex }
        _endIndex = { base.endIndex }
        _index = { base.index($0, offsetBy: $1) }
        _subscript = { base[$0] }
    }
    
    typealias Index = Int
    typealias Element = T
    
    var startIndex: Index { _startIndex() }
    var endIndex: Index { _endIndex() }
    func index(after i: Index) -> Index { i + 1 }
    subscript(position: Index) -> T { _subscript(position) }
}

在这个示例中,我们定义了一个名为 AnyCollection 的结构体,并将其声明为遵循 Collection 协议。该结构体包含了一些私有的存储属性和初始化方法。

其中,init 方法的参数 base 是一个任意类型的集合,由于我们无法确定其具体类型,因此我们使用泛型来代替。在该方法内部,我们利用泛型约束 where C.Element == T 来保证 base 中的元素类型必须和 T 相同。

我们还定义了一些私有的属性和方法,这些属性和方法可以将 AnyCollection 和集合类型(无论是 Array 还是 Set)联系起来。例如,_startIndex_endIndex 方法分别返回集合的起始位置和终止位置,而 _subscript 方法则实现了下标操作符。

通过类型擦除,我们可以使用 AnyCollection 来存储任何类型的集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let array: [Int] = [1, 2, 3]
let set: Set<String> = ["hello", "world"]

let anyArray = AnyCollection(array)
let anySet = AnyCollection(set)

for item in anyArray {
    print(item)
}
// 输出 "1 2 3"

for item in anySet {
    print(item)
}
// 输出 "hello world"

递归泛型类型

递归泛型类型(recursive generic types)是指一个泛型类型的某个成员变量或参数也是该泛型类型本身,这种类型常常用于表示树形结构或链表等数据结构。在Swift中,可以使用枚举类型来实现递归泛型类型。

例如,我们可以定义一个二叉树的节点:

1
2
3
4
enum BinaryTree<Element\> {
    case empty
    indirect case node(Element, left: BinaryTree<Element\>, right: BinaryTree<Element\>)
}

在上面的代码中,BinaryTree是一个枚举类型,它有两个可能的值:空节点(empty)和非空节点(node)。由于非空节点包含左右子树,因此需要使用indirect关键字来指定该枚举类型是递归的。这样,在创建一个新的BinaryTree节点时,我们可以将它的左右子树指定为另外两个BinaryTree节点,从而构建出一个完整的二叉树数据结构。

递归泛型类型在Swift编程中应用广泛,常常用于表示复杂的数据结构,如树形结构、图形结构、网络协议等。

本文由作者按照 CC BY 4.0 进行授权

Swift中的协议

Swift中的不透明类型