引言:为什么在 Swift 中离不开泛型?
想象一下,你需要编写一个函数来相加两个数字。然后——再写一个函数来拼接两个字符串。接着——还要处理数组。如果没有泛型编程(Generics),你就必须为每种数据类型创建单独的代码版本。这不仅会增加代码量,还会使维护变得更加复杂。
Swift 中的泛型(Generics)是一个强大的工具,它允许你编写灵活、可复用且类型安全的代码。它们是诸如 Array、Dictionary 和 Optional 等流行结构的基础。在本文中,我们将探讨为什么需要泛型,如何在实际中使用它们,以及为什么每个初学 iOS/macOS 的开发人员都应该掌握这个主题。
用简单的话来说,什么是泛型?
泛型(Generics)是一种编写代码的方式,它能够处理任何数据类型,同时又不牺牲 Swift 严格的类型检查。你不必指定具体的类型(例如 Int 或 String),而是使用一个占位符(通常是字母 T),在调用函数或创建结构时,这个占位符会被实际的类型所替换。
没有泛型时的问题
假设你要编写一个交换两个变量值的函数。没有泛型,你就得这样写:
func swapInts(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
func swapStrings(_ a: inout String, _ b: inout String) {
let temp = a
a = b
b = temp
}
这显然是重复的代码。如果需要交换 Double 或自定义类型,函数的数量还会继续增加。
使用泛型的解决方案
一个通用的函数就能替代上面所有的函数:
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
// 使用:
var x = 10
var y = 20
swapValues(&x, &y) // x = 20, y = 10
var firstName = "安娜"
var lastName = "伊万诺娃"
swapValues(&firstName, &lastName) // firstName = "伊万诺娃"
尖括号 <T> 声明了一个类型占位符。编译器会自动确定在第一种情况下 T 是 Int,在第二种情况下是 String。代码变得简洁且安全。
如何使用泛型:主要场景
Swift 中的泛型可以应用于函数、结构体、类、枚举和协议。让我们看看最常见的几种情况。
1. 泛型函数
我们已经看到了 swapValues 的例子。但我们可以更进一步——创建一个在数组中查找元素索引的函数:
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (index, element) in array.enumerated() {
if element == value {
return index
}
}
return nil
}
let numbers = [1, 5, 9, 12]
if let index = findIndex(of: 9, in: numbers) {
print("索引:\(index)") // 索引:2
}
请注意这里的 <T: Equatable>。这是一个类型约束(type constraint)。它的意思是:“T 必须遵循 Equatable 协议,这样我们才能使用 == 运算符”。如果没有这个约束,代码将无法编译。
2. 泛型类型(结构体和类)
让我们创建一个简单的“栈”(LIFO)结构,它可以存储任何类型:
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
return items.popLast()
}
func peek() -> Element? {
return items.last
}
}
// 使用:
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop
```