Swift: Swift 泛型教程:从零掌握泛型函数与类型约束
泛型让你编写适应任意类型的灵活代码,避免为每种类型重复实现相同逻辑——像"万能模具"一样,倒入什么材料就产出什么形状。
1. 你将学到
- 如何定义泛型函数和泛型类型
- 类型约束如何限制泛型参数的适用范围
- 关联类型在协议中的用法
- where 子句的进阶使用方法
- 标准库中泛型的实际应用场景
2. 一个后端工程师的真实故事
(1) 痛点:为每种类型写相同逻辑
Charlie 在开发一个缓存服务时,需要实现一个"先进后出"的栈数据结构。他先为 Int 写了一个版本:
SWIFT
struct IntStack {
private var items: [Int] = []
mutating func push(_ item: Int) { items.append(item) }
mutating func pop() -> Int? { items.popLast() }
}
但很快他需要支持 String、Double,甚至自定义 User 类型。Charlie 发现自己在不断复制粘贴——只是换了类型名,逻辑完全一样。
(2) 泛型的解法
泛型把类型变成参数,一份代码适配所有类型:
SWIFT
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { items.popLast() }
}
现在 Stack<Int>、Stack<String>、Stack<User> 都用同一份实现,编译器保证类型安全。
(3) 收益:代码量减少 60%
| 维度 | 非泛型方案 | 泛型方案 |
|---|---|---|
| 代码行数 | 120 行(3 种类型) | 20 行(1 个泛型) |
| 新增类型成本 | 40 行复制粘贴 | 1 行 Stack<NewType> |
| 类型安全 | ✅ 每种类型独立 | ✅ 编译时检查 |
| 维护成本 | ❌ 改逻辑需改 3 处 | ✅ 只改 1 处 |
3. 泛型函数 (Generic Functions)
泛型函数允许在函数定义中使用占位符类型,调用时才确定具体类型。
graph LR
A["func swap<T>(a: inout T, b: inout T)"] --> B["Called with Int"]
A --> C["Called with String"]
A --> D["Called with Double"]
B --> E["T = Int: safe swap"]
C --> F["T = String: safe swap"]
D --> G["T = Double: safe swap"]
(1) 泛型参数语法
泛型参数写在函数名后的尖括号 <> 中,通常用大写字母 T、U、V 占位。
| 写法 | 含义 | 示例 |
|---|---|---|
<T> |
单个泛型参数 | func identity<T>(_ value: T) -> T |
<T, U> |
两个泛型参数 | func pair<T, U>(_ a: T, _ b: U) -> (T, U) |
<T: Equatable> |
有约束的泛型 | func isEqual<T: Equatable>(_ a: T, _ b: T) -> Bool |
▶ 示例:交换两个变量的值
SWIFT
// ============================================
// 泛型函数:交换任意类型的两个值
// ============================================
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)
print("x = \(x), y = \(y)")
var firstName = "Alice"
var lastName = "Bob"
swapValues(&firstName, &lastName)
print("firstName = \(firstName), lastName = \(lastName)")
输出:
TEXT 📖 仅展示x = 20, y = 10 firstName = Bob, lastName = Alice
(2) 多泛型参数
函数可以有多个泛型参数,分别代表不同位置的不同类型。
▶ 示例:构建键值对
SWIFT
// ============================================
// 多个泛型参数构建键值对
// ============================================
func makePair<K, V>(_ key: K, _ value: V) -> (K, V) {
return (key, value)
}
let pair1 = makePair("id", 1001)
let pair2 = makePair(3.14, "Pi")
print(pair1)
print(pair2)
输出:
TEXT 📖 仅展示(id, 1001) (3.14, Pi)
4. 泛型类型 (Generic Types)
泛型类型允许你用占位符定义结构体、类或枚举,使其能处理任意类型的数据。
graph TB
A["Stack<Element>"] --> B["push(Element)"]
A --> C["pop() -> Element?"]
A --> D["peek() -> Element?"]
A --> E["count: Int"]
B --> F["append to items"]
C --> G["remove last from items"]
D --> H["return items.last"]
(1) 泛型结构体
SWIFT
struct Stack<Element> { ... }
| 使用场景 | 写法 | 说明 |
|---|---|---|
| Int 栈 | Stack<Int> |
只能 push/pop Int 值 |
| String 栈 | Stack<String> |
只能 push/pop String 值 |
| 自定义类型栈 | Stack<User> |
只能 push/pop User 值 |
▶ 示例:泛型栈数据结构
SWIFT
// ============================================
// 泛型栈:支持任意类型的先进后出
// ============================================
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
func peek() -> Element? {
items.last
}
var count: Int { items.count }
}
var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
intStack.push(30)
print("Pop: \(intStack.pop() ?? 0)")
var stringStack = Stack<String>()
stringStack.push("Alice")
stringStack.push("Bob")
print("Peek: \(stringStack.peek() ?? "")")
输出:
TEXT 📖 仅展示Pop: 30 Peek: Bob
(2) 泛型扩展
扩展泛型类型时,不需要重复声明类型参数——直接用占位符名即可。
▶ 示例:为泛型栈添加 map 方法
SWIFT
// ============================================
// 通过扩展为 Stack 添加 map 功能
// ============================================
extension Stack {
func map<T>(_ transform: (Element) -> T) -> Stack<T> {
var result = Stack<T>()
for item in items {
result.push(transform(item))
}
return result
}
}
var s = Stack<Int>()
s.push(1); s.push(2); s.push(3)
let doubled = s.map { $0 * 2 }
print("Count: \(doubled.count)")
输出:
TEXT 📖 仅展示Count: 3
5. 类型约束与关联类型
类型约束限制泛型参数必须满足特定条件。关联类型是协议中的"泛型占位符",由实现者指定具体类型。
graph TB
A["<T: Equatable>"] --> B["Can use =="]
A --> C["func findIndex<T: Equatable>(of: T, in: [T]) -> Int?"]
B --> D["Int =="]
B --> E["String =="]
B --> F["Custom with Equatable"]
(1) 类型约束
| 约束写法 | 含义 | 使用场景 |
|---|---|---|
<T: Equatable> |
T 必须可比较相等 | 查找元素、去重 |
<T: Hashable> |
T 必须可哈希 | 作为 Dictionary 的 key |
<T: Comparable> |
T 必须可比较大小 | 排序、求最大最小值 |
<T: Codable> |
T 必须可编码解码 | JSON 序列化 |
▶ 示例:使用 Equatable 约束查找元素
SWIFT
// ============================================
// 约束泛型参数必须可比较相等
// ============================================
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (index, item) in array.enumerated() {
if item == value {
return index
}
}
return nil
}
let numbers = [10, 20, 30, 40]
if let index = findIndex(of: 30, in: numbers) {
print("Found at index \(index)")
}
let names = ["Alice", "Bob", "Charlie"]
if let index = findIndex(of: "Bob", in: names) {
print("Found at index \(index)")
}
输出:
TEXT 📖 仅展示Found at index 2 Found at index 1
(2) 关联类型与 where 子句
关联类型 associatedtype 让协议支持泛型——协议本身不指定具体类型,由实现者决定。
▶ 示例:带关联类型的协议与 where
SWIFT
// ============================================
// 关联类型协议 + where 子句
// ============================================
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct Box<T>: Container {
typealias Item = T
private var items: [T] = []
mutating func append(_ item: T) { items.append(item) }
var count: Int { items.count }
subscript(i: Int) -> T { items[i] }
}
func allItemsMatch<C1: Container, C2: Container>(
_ c1: C1, _ c2: C2
) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {
guard c1.count == c2.count else { return false }
for i in 0..<c1.count {
if c1[i] != c2[i] { return false }
}
return true
}
var box1 = Box<Int>()
box1.append(1); box1.append(2); box1.append(3)
var box2 = Box<Int>()
box2.append(1); box2.append(2); box2.append(3)
print("All match: \(allItemsMatch(box1, box2))")
输出:
TEXT 📖 仅展示All match: true
6. 完整示例:泛型队列与缓存系统
SWIFT
// ============================================
// 完整示例:泛型队列 + 限长缓存
// 功能:通用队列、缓存封装、统计
// ============================================
import Foundation
// 1. 泛型队列
struct Queue<Element> {
private var items: [Element] = []
mutating func enqueue(_ item: Element) { items.append(item) }
mutating func dequeue() -> Element? { items.isEmpty ? nil : items.removeFirst() }
var count: Int { items.count }
}
// 2. 泛型缓存(带容量限制)
class Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
private let capacity: Int
init(capacity: Int = 100) {
self.capacity = capacity
}
func set(_ value: Value, for key: Key) {
if storage.count >= capacity {
storage.removeFirst()
}
storage[key] = value
}
func get(for key: Key) -> Value? { storage[key] }
var count: Int { storage.count }
}
// 3. 使用示例
var queue = Queue<String>()
queue.enqueue("Task 1")
queue.enqueue("Task 2")
queue.enqueue("Task 3")
print("Queue count: \(queue.count)")
print("Dequeue: \(queue.dequeue() ?? "")")
let cache = Cache<String, Int>(capacity: 3)
cache.set(42, for: "answer")
cache.set(100, for: "score")
print("Cached answer: \(cache.get(for: "answer") ?? 0)")
print("Cache count: \(cache.count)")
输出:
TEXT 📖 仅展示Queue count: 3 Dequeue: Task 1 Cached answer: 42 Cache count: 2
❓ 常见问题
Q 泛型和 Any 有什么区别?
A 泛型保留类型信息,编译时类型安全;Any 在运行时检查类型。泛型是"编译器知道具体类型",Any 是"编译器放弃类型检查"。
Q 什么时候用泛型参数 vs 协议约束?
A 泛型参数适用于"类型可任意但操作一致"的场景;协议约束适用于"类型必须满足特定接口"的场景。两者常结合使用。
Q 关联类型和泛型参数有什么区别?
A 关联类型定义在协议内部,由实现者指定具体类型;泛型参数定义在函数/类型上,由调用者指定。关联类型相当于"协议的泛型参数"。
Q 泛型数组 Array 是怎么实现的?
A 标准库的 Array 就是一个泛型结构体
Array<Element>,Element 是泛型参数。所有数组操作(append、sort、map)都是泛型方法。Q 泛型会影响性能吗?
A 不会。Swift 使用"泛型特化"(Specialization),编译器为每个具体类型生成专门版本的代码,运行时无泛型开销。
📖 小节
- 泛型函数用
<T>声明类型参数,调用时自动推断具体类型 - 泛型类型(结构体/类/枚举)可以处理任意类型的数据,同一份代码适配多种类型
- 类型约束
<T: Protocol>限制泛型参数的适用范围,保证类型安全 - 关联类型
associatedtype让协议支持泛型,由实现者决定具体类型 - where 子句用于表达复杂的多条件类型关系
- 标准库大量使用泛型:Array、Dictionary、Optional 都是泛型类型
📝 作业
- 基础题: 写一个泛型函数
findMax<T: Comparable>,接受两个参数并返回较大的值。测试 Int、Double、String 三种类型。 - 进阶题: 扩展第 4 节的
Stack类型,添加filter方法,接受(Element) -> Bool闭包,返回过滤后的新栈。 - 挑战题: 使用泛型实现一个
RingBuffer<Element>环状缓冲区,支持write(_:)和read() -> Element?方法,固定容量,写入满时自动覆盖最旧数据。