现在位置: 首页 > Swift 教程 > 正文

Swift 泛型

泛型(Generics)让代码在编写时可以不绑定具体类型,等真正调用时再决定用哪种类型。

同一个泛型函数或泛型类型,可以处理 Int、String、自定义结构体等任意类型,同时保持编译期类型检查。

Swift 标准库里的 Array、Dictionary、Optional 都是泛型类型,所以你在使用它们时其实一直在用泛型。


为什么需要泛型

假设要写一个交换两个变量值的函数。如果不使用泛型,就得为每种类型各写一份。

实例

// 只支持 Int 的版本
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 的实际类型。

实例

import Foundation

// 泛型函数:交换两个同类型变量的值
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 约束。

实例

import Foundation

// 泛型函数:查找元素在数组中的下标
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> 可以装任意类型,但同一个实例内的元素类型必须一致。

实例

import Foundation

// 泛型结构体:不限定 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 比较大小。

实例

import Foundation

// 类型约束: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 子句表达更清楚。

下面同时演示两种用法:给泛型扩展加条件,以及给泛型函数加约束。

实例

import Foundation

// 泛型结构体:栈
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、添加方法、计数属性和下标。

实例

import Foundation

// 定义带关联类型的协议
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 子句是同一套机制,只是写法上分成了「普通扩展」和「条件扩展」两种。

实例

import Foundation

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 约束,编译器才允许使用相等比较。