首页 / Swift 编程语言教程 / 泛型基础 Generics

Swift 编程语言教程

泛型基础 Generics

本教程共 93 篇 · 第 70 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

SwiftSwift 编程语言教程generics泛型类型参数占位类型泛型类型Stack

本节目标:学会用占位类型写出”一套逻辑、多种类型”的泛型函数和泛型类型,理解类型参数是怎么被推断出来的。

你大概早就在用泛型了——ArrayDictionary 就是泛型集合:你能造一个装 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。它只表达一件事——ab 必须是同一个类型 T,至于 T 具体是谁,每次调用时由传入的值决定。

调用方式和普通函数一样,只是现在能传任意类型,前提是 ab 类型一致:

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)   // T 推断为 Int

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)  // T 推断为 String

Swift 是类型安全的:ab 必须同类型,想拿 StringDouble 互交换会直接编译报错。这一点泛型没有放松,反而被编译器严格把关。

Note

标准库其实已经提供了 swap(_:_:),你日常直接用就行。这里重写它,纯粹是为了讲清泛型函数的写法。

1-3 类型参数

上面例子里的 T 就是”类型参数”(type parameter)。它写在函数名后面、那对尖括号里,作用是给占位类型起个名字。一旦声明了,你就可以在函数参数、返回值、函数体内的类型标注里使用它。每次调用函数,T 都会被替换成真实的类型。

类型参数不止一个。需要多种占位类型时,在尖括号里用逗号隔开,比如 <Key, Value><T, U>。它们之间互不相同,分别由调用处的实际类型填进来。

1-4 给类型参数起好名字

类型参数大多该起”有意义的名字”。比如 Dictionary<Key, Value> 里的 KeyValueArray<Element> 里的 Element,一看就知道占位类型和泛型整体是什么关系。当占位类型实在没有语义可言时,才用单字母传统命名,比如 TUV

命名规则上,类型参数用”大驼峰”,像 TMyTypeParameter,以此暗示它代表”类型”而不是”值”。这一点和变量名的小驼峰区分开。

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>)则让自定义类型适配任意元素类型。类型参数应起有意义的名字,扩展泛型类型时可直接沿用原占位名。下章我们给这些占位类型加约束,让泛型既能通用又有底线。