Swift 泛型
泛型(Generics)让代码在编写时可以不绑定具体类型,等真正调用时再决定用哪种类型。
同一个泛型函数或泛型类型,可以处理 Int、String、自定义结构体等任意类型,同时保持编译期类型检查。
Swift 标准库里的 Array、Dictionary、Optional 都是泛型类型,所以你在使用它们时其实一直在用泛型。
为什么需要泛型
假设要写一个交换两个变量值的函数。如果不使用泛型,就得为每种类型各写一份。
实例
func runoobSwapInt(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
// 只支持 String 的版本
func runoobSwapString(_ a: inout String, _ b: inout String) {
let temp = a
a = b
b = temp
}
两份代码逻辑完全一样,只是类型不同。类型越多,重复代码越多。
泛型把类型也变成一个「参数」,一份代码就能覆盖所有类型。
泛型函数
在函数名后的尖括号里声明类型参数,例如 <T>,之后就能像使用普通类型一样使用 T。
下面的 runoobSwap 同时支持字符串和整数,调用时编译器会自动推断 T 的实际类型。
实例
// 泛型函数:交换两个同类型变量的值
func runoobSwap<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var site1 = "www.runoob.com"
var site2 = "RUNOOB"
runoobSwap(&site1, &site2)
print("\(site1) / \(site2)")
// 类型参数 T 会被推断为 Int
var x = 10
var y = 99
runoobSwap(&x, &y)
print("x = \(x), y = \(y)")
运行结果:
RUNOOB / www.runoob.com x = 99, y = 10
如果希望两个参数是不同类型,就需要声明两个类型参数,例如 func f<T, U>(_ a: T, _ b: U)。
类型参数与类型推断
类型参数只是一个占位符,它的实际类型由调用时的实参决定。
下面这个泛型函数在数组中查找某个元素,返回它的下标。因为要用 == 比较,所以加上 T: Equatable 约束。
实例
// 泛型函数:查找元素在数组中的下标
func runoobIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (index, item) in array.enumerated() where item == value {
return index
}
return nil
}
let sites = ["baidu", "runoob", "taobao"]
if let i = runoobIndex(of: "runoob", in: sites) {
print("runoob 的下标是 \(i)")
}
运行结果:
runoob 的下标是 1
调用时 T 被推断为 String;如果传入 [Int],T 就变成 Int,无需写任何额外代码。
泛型类型
不只是函数,结构体、类、枚举都可以是泛型的。在类型名后声明类型参数即可。
下面的泛型栈 RunoobStack<Element> 可以装任意类型,但同一个实例内的元素类型必须一致。
实例
// 泛型结构体:不限定 Element 是什么类型
struct RunoobStack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
var top: Element? { items.last }
}
var intStack = RunoobStack<Int>() // 显式指定类型参数
intStack.push(1)
intStack.push(2)
intStack.push(3)
print("栈顶:\(intStack.top!)")
var strStack = RunoobStack<String>() // 另一个完全独立的类型
strStack.push("runoob")
strStack.push("RUNOOB")
print("栈顶:\(strStack.top!)")
print("弹出一个:\(strStack.pop()!)")
print("栈顶:\(strStack.top!)")
// 泛型类
class RunoobBox<Value> {
var value: Value
init(_ value: Value) {
self.value = value
}
}
let box = RunoobBox("菜鸟教程") // Value 推断为 String
print(box.value)
运行结果:
栈顶:3 栈顶:RUNOOB 弹出一个:RUNOOB 栈顶:runoob 菜鸟教程
RunoobStack<Int> 和 RunoobStack<String> 在编译器看来是两个完全不同的类型,不能互相赋值。
注意:创建泛型实例时如果无法从上下文推断类型,必须显式写出,例如 RunoobStack<Int>();如果写成 RunoobStack() 而后面又没有用到类型信息,编译器会报「无法推断泛型参数」。
类型约束
默认情况下,类型参数可以是任意类型,因此能调用的操作非常有限。
加上类型约束后,就能对 T 使用约束协议提供的成员,比如用 Comparable 比较大小。
实例
// 类型约束:T 必须遵循 Comparable
func runoobMax<T: Comparable>(_ a: T, _ b: T) -> T {
return a > b ? a : b
}
print(runoobMax(3, 9))
print(runoobMax("apple", "runoob"))
// 多个约束:T 同时遵循 Hashable 与 CustomStringConvertible
func runoobInfo<T: Hashable & CustomStringConvertible>(_ value: T) -> String {
return "值:\(value),描述:\(value.description)"
}
print(runoobInfo("RUNOOB"))
// 约束一个类型必须是某个类的子类
class RunoobBase {}
class RunoobSub: RunoobBase {}
func runoobCheck<T: RunoobBase>(_ item: T) -> String {
return "类型是 \(type(of: item))"
}
print(runoobCheck(RunoobSub()))
运行结果:
9 runoob 值:RUNOOB,描述:RUNOOB 类型是 RunoobSub
约束有两种常见写法,效果相同,前者更简洁。
| 写法 | 示例 | 说明 |
|---|---|---|
| 尖括号内约束 | func f<T: Comparable>(_ x: T) | 最常用,约束写在类型参数后面 |
| where 子句约束 | func f<T>(_ x: T) where T: Comparable | 约束复杂时更易读 |
| 协议组合 | T: Hashable & CustomStringConvertible | 用 & 连接多个协议 |
| 类约束 | T: RunoobBase | 要求 T 是该类或其子类 |
where 子句
当约束条件比较多,或者约束对象不是单个类型参数时,用 where 子句表达更清楚。
下面同时演示两种用法:给泛型扩展加条件,以及给泛型函数加约束。
实例
// 泛型结构体:栈
struct RunoobStack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
var all: [Element] { items }
}
// 用 where 子句约束:只有当 Element 遵循 Equatable 时才有 contains
extension RunoobStack where Element: Equatable {
func contains(_ item: Element) -> Bool {
items.contains(item)
}
}
// 用 where 子句约束:只有当 Element 遵循 Comparable 时才能求最大值
extension RunoobStack where Element: Comparable {
func maxElement() -> Element? {
items.max()
}
}
var stack = RunoobStack<Int>()
stack.push(30)
stack.push(10)
stack.push(20)
print("是否包含 10:\(stack.contains(10))")
print("最大值:\(stack.maxElement()!)")
// 函数中的 where 子句
func runoobPairs<C1: Collection, C2: Collection>(_ a: C1, _ b: C2) -> Int
where C1.Element: Equatable, C1.Element == C2.Element {
var count = 0
for x in a {
if b.contains(where: { $0 == x }) {
count += 1
}
}
return count
}
print("相同元素个数:\(runoobPairs([1, 2, 3, 4], [2, 4, 6]))")
运行结果:
是否包含 10:true 最大值:30 相同元素个数:2
函数上的 where 子句还写出了 C1.Element == C2.Element,表示两个集合的元素类型必须相同,这样才允许互相比较。
关联类型 associatedtype
协议本身不能带类型参数,但可以用 associatedtype 声明一个「占位类型」,具体是什么由遵循者决定。
下面的协议要求遵循者提供元素类型 Item、添加方法、计数属性和下标。
实例
// 定义带关联类型的协议
protocol RunoobContainer {
associatedtype Item // 关联类型,具体是什么由遵循者决定
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
// 遵循协议:Item 被推断为 Int
struct RunoobIntList: RunoobContainer {
private var items: [Int] = []
mutating func append(_ item: Int) {
items.append(item)
}
var count: Int { items.count }
subscript(i: Int) -> Int { items[i] }
}
var list = RunoobIntList()
list.append(100)
list.append(200)
list.append(300)
print("元素个数:\(list.count)")
print("第一个元素:\(list[0])")
// 用类型别名显式指定关联类型
struct RunoobStringList: RunoobContainer {
typealias Item = String // 显式写出关联类型
private var items: [String] = []
mutating func append(_ item: String) { items.append(item) }
var count: Int { items.count }
subscript(i: Int) -> String { items[i] }
}
var names = RunoobStringList()
names.append("Runoob")
names.append("RUNOOB")
print("元素个数:\(names.count)")
print("第二个元素:\(names[1])")
// 泛型函数配合关联类型协议
func runoobTotal<C: RunoobContainer>(_ container: C) -> Int {
return container.count
}
print("总数:\(runoobTotal(list))")
运行结果:
元素个数:3 第一个元素:100 元素个数:2 第二个元素:RUNOOB 总数:3
RunoobIntList 没有写 typealias,编译器根据 append 和下标的方法签名自动推断出 Item = Int。
注意:如果协议里既有 associatedtype 又要用泛型函数接收它,通常把关联类型写成类型参数 C: RunoobContainer,让编译器去推断,不要试图直接写 RunoobContainer 当作具体类型。
泛型扩展
泛型类型可以被扩展,扩展里能直接使用原有的类型参数,也能通过 where 增加额外条件。
这与上文的 where 子句是同一套机制,只是写法上分成了「普通扩展」和「条件扩展」两种。
实例
struct RunoobStack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
}
// 普通扩展:所有 Element 都能用
extension RunoobStack {
var isEmpty: Bool { items.isEmpty }
var top: Element? { items.last }
}
// 条件扩展:Element 是字符串时才追加一个格式化方法
extension RunoobStack where Element == String {
func joined() -> String {
items.joined(separator: " - ")
}
}
var stack = RunoobStack<String>()
stack.push("Runoob")
stack.push("RUNOOB")
stack.push("www.runoob.com")
print("是否为空:\(stack.isEmpty)")
print("栈顶:\(stack.top!)")
print("拼接结果:\(stack.joined())")
运行结果:
是否为空:false 栈顶:www.runoob.com 拼接结果:Runoob - RUNOOB - www.runoob.com
where Element == String 是精确相等约束,比 Element: Equatable 更严格,只对元素类型恰好为 String 的栈生效。
常见问题
集中回答几个容易混淆的点。
泛型和协议有什么分工
泛型在编译期确定具体类型,没有运行时开销,适合「同一套算法处理多种类型」;协议则用于描述抽象能力,两者经常配合使用。
什么时候用泛型,什么时候用 any
如果调用点能确定具体类型、又希望保留类型信息,用泛型;如果需要在运行时装不同具体类型、甚至放进同一个数组,用存在类型 any,详见《Swift some 与 any》。
泛型会不会影响性能
不会。Swift 通过泛型特化(specialization)在编译期为具体类型生成专门代码,运行时不比手写版本慢。
为什么泛型里不能直接比较两个值
因为类型参数默认不保证支持 ==,需要加上 T: Equatable 约束,编译器才允许使用相等比较。
