泛型基础 Generics
本教程共 93 篇 · 第 70 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:学会用占位类型写出”一套逻辑、多种类型”的泛型函数和泛型类型,理解类型参数是怎么被推断出来的。
你大概早就在用泛型了——Array 和 Dictionary 就是泛型集合:你能造一个装 Int 的数组,也能造一个装 String 的数组,根本不用为每种类型重写一遍数组。泛型(generics)就是 Swift 用来表达”同一套逻辑适配多种类型”的机制,它是标准库的基石,也是写复用代码最有力的工具。
1-1 泛型解决什么麻烦
先看一个非泛型函数:交换两个 Int 的值。
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
它用 inout 参数原地交换,很好用,但只能用于 Int。想交换 String?再写个 swapTwoStrings;想交换 Double?再写个 swapTwoDoubles。这三个函数体一模一样,唯一区别是参数类型。这种”复制粘贴只为换类型”的写法,既啰嗦又难维护——哪天逻辑要改,三处都得同步改。
泛型就是来消灭这种重复的:写一次函数,让它对任意相同类型的值都能工作。
1-2 第一个泛型函数
把上面的函数改成泛型版本,用一个占位类型名 T 代替具体的 Int:
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
尖括号里的 <T> 告诉 Swift:T 是个”占位类型名”,不是真的有个类型叫 T。它只表达一件事——a 和 b 必须是同一个类型 T,至于 T 具体是谁,每次调用时由传入的值决定。
调用方式和普通函数一样,只是现在能传任意类型,前提是 a、b 类型一致:
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt) // T 推断为 Int
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString) // T 推断为 String
Swift 是类型安全的:a 和 b 必须同类型,想拿 String 和 Double 互交换会直接编译报错。这一点泛型没有放松,反而被编译器严格把关。
Note标准库其实已经提供了
swap(_:_:),你日常直接用就行。这里重写它,纯粹是为了讲清泛型函数的写法。
1-3 类型参数
上面例子里的 T 就是”类型参数”(type parameter)。它写在函数名后面、那对尖括号里,作用是给占位类型起个名字。一旦声明了,你就可以在函数参数、返回值、函数体内的类型标注里使用它。每次调用函数,T 都会被替换成真实的类型。
类型参数不止一个。需要多种占位类型时,在尖括号里用逗号隔开,比如 <Key, Value>、<T, U>。它们之间互不相同,分别由调用处的实际类型填进来。
1-4 给类型参数起好名字
类型参数大多该起”有意义的名字”。比如 Dictionary<Key, Value> 里的 Key、Value,Array<Element> 里的 Element,一看就知道占位类型和泛型整体是什么关系。当占位类型实在没有语义可言时,才用单字母传统命名,比如 T、U、V。
命名规则上,类型参数用”大驼峰”,像 T、MyTypeParameter,以此暗示它代表”类型”而不是”值”。这一点和变量名的小驼峰区分开。
Tip起名别偷懒:把一个泛型栈的元素占位叫
Element,比叫T清楚得多。读你代码的人(包括未来的你)会感谢这个习惯。
起名时还有一个小技巧:当占位类型会在多处出现,语义名能显著降低读代码时的认知负担。反过来,如果泛型函数很短、类型参数一眼就能看出”就是同一个未知类型”,用 T 反而比硬起名字更轻快。命名没有绝对对错,唯一标准是”读的人能不能秒懂”。
1-5 泛型类型:写一个栈
除了泛型函数,你还能定义自己的”泛型类型”——自定义的类、结构体、枚举,能适配任意类型。最有代表性的例子是栈(stack):一种后进先出(LIFO)的集合,只能从末尾压入(push)和弹出(pop)。
先看看非泛型的 Int 栈:
struct IntStack {
var items: [Int] = []
mutating func push(_ item: Int) { items.append(item) }
mutating func pop() -> Int { items.removeLast() }
}
它只能装 Int。改成泛型版本,把 Int 换成占位 Element:
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element { items.removeLast() }
}
Element 出现在三处:声明一个 Element 数组、规定 push 参数类型、规定 pop 返回类型。创建实例时,把要装的类型写进尖括号:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
let fromTheTop = stackOfStrings.pop() // "tres"
Stack<String>() 告诉编译器这个栈装 String,于是 push 只能接收字符串、pop 也返回字符串。换 Stack<Int>() 就变成装整数。一个定义,服务所有类型——这就是泛型类型的威力。
1-6 扩展泛型类型
给泛型类型写扩展时,不用再重复写类型参数列表,原类型定义里的 Element 在扩展体内直接可用:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
这个扩展给 Stack 加了个只读计算属性 topItem,返回栈顶元素但不弹出它。注意扩展头部没写 <Element>——Swift 自动沿用原类型定义的类型参数名。
if let topItem = stackOfStrings.topItem {
print("栈顶是 \(topItem)。")
}
任何 Stack 实例都能用 topItem,无论它装的是什么类型。泛型类型与其扩展共享同一套类型参数,扩展里可以自由引用 Element。
1-7 什么时候该用泛型
泛型虽好,但别为了用而用。一个判断准则是:当你发现自己要为一堆”只是类型不同、逻辑完全相同”的函数或类型写第二遍、第三遍时,那就是泛型出场的时候。比如交换、查找、比较、容器——这些操作的本质和具体类型无关,抽成占位类型最自然。
反过来,如果逻辑本身就依赖某个具体类型的能力(比如一定要用到字符串的 count),那它本来就该写死成那个类型,硬改成泛型反而要额外加约束、徒增复杂度。泛型的目标是”消除无意义的重复”,不是为了显得高级。初学者常犯的错是把明明只服务于一种类型的函数硬写成泛型,结果约束写了一堆,可读性反而下降。记住:先有重复,再有泛型;没有重复,就别勉强。
1-8 小结
泛型用占位类型写出”一套逻辑、多种类型”的代码,消灭为换类型而复制粘贴的重复。泛型函数把占位类型写在函数名后的尖括号里(如 <T>),由调用处推断具体类型;泛型类型(如 Stack<Element>)则让自定义类型适配任意元素类型。类型参数应起有意义的名字,扩展泛型类型时可直接沿用原占位名。下章我们给这些占位类型加约束,让泛型既能通用又有底线。