Swift 泛型(千字长文)

什么是 Swift 泛型?从基础到实战的完整解析

在 Swift 编程语言中,泛型是一种强大的抽象机制,它让你能够编写可复用的代码,而无需提前指定具体的数据类型。想象一下,你正在设计一个“快递盒”——如果这个盒子只能装苹果,那它就太局限了。但如果你设计的是一个“通用盒子”,不管里面是书本、衣服还是电子设备,它都能完美适配,那是不是更高效?

Swift 泛型就是这种“通用盒子”的实现方式。它允许你定义函数、结构体、类和枚举时,不绑定具体的类型,而是用一个占位符来表示“某个类型”,在调用时再指定具体类型。这种设计极大提升了代码的复用性和类型安全性。

举个例子:你写一个函数用来交换两个值,如果只能交换 Int 类型,那每次想交换字符串或浮点数时,就得重写一个函数。而有了泛型,只需写一次,就能应对所有类型。

泛型函数:让函数“通吃”多种类型

泛型函数是 Swift 泛型最常见、最实用的形式。它的语法核心在于在函数名后加上尖括号 <T>,其中 T 是类型参数的占位符,你可以随意命名(通常用 T、U、Value 等)。

下面是一个经典的交换函数示例:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

我们来逐行解析:

  • func swapTwoValues<T>:定义一个泛型函数,<T> 表示这是一个泛型,T 是待填充的类型。
  • (_ a: inout T, _ b: inout T):参数 a 和 b 都是类型 T 的可变引用(inout),意味着函数可以修改传入的变量。
  • let temporaryA = a:先保存 a 的值,避免被覆盖。
  • a = b:把 b 的值赋给 a。
  • b = temporaryA:把原来 a 的值赋给 b,完成交换。

现在你可以用这个函数交换任意类型的值:

var firstNumber = 10
var secondNumber = 20
swapTwoValues(&firstNumber, &secondNumber)
print("交换后:\(firstNumber), \(secondNumber)") // 输出:交换后:20, 10

var firstString = "Hello"
var secondString = "World"
swapTwoValues(&firstString, &secondString)
print("交换后:\(firstString), \(secondString)") // 输出:交换后:World, Hello

注意:inout 关键字必须配合 & 取地址符使用,这是 Swift 的安全机制,确保你明确知道在修改原值。

泛型结构体:构建可复用的容器

结构体是 Swift 中常用的值类型,而泛型结构体能让你创建通用的数据容器。比如一个“存储箱”——它可以装整数、字符串、甚至自定义对象。

下面是一个简单的泛型栈(Stack)结构体:

struct Stack<T> {
    private var elements: [T] = []
    
    // 入栈操作
    mutating func push(_ item: T) {
        elements.append(item)
    }
    
    // 出栈操作
    mutating func pop() -> T? {
        return elements.popLast()
    }
    
    // 查看栈顶元素(不移除)
    func peek() -> T? {
        return elements.last
    }
    
    // 检查栈是否为空
    var isEmpty: Bool {
        return elements.isEmpty
    }
    
    // 返回栈中元素数量
    var count: Int {
        return elements.count
    }
}

让我们来实际使用它:

var stringStack = Stack<String>()
stringStack.push("Swift")
stringStack.push("泛型")
stringStack.push("很强大")

print("栈顶元素:\(stringStack.peek() ?? "空")") // 栈顶元素:泛型
print("栈大小:\(stringStack.count)") // 栈大小:3

if let popped = stringStack.pop() {
    print("弹出:\(popped)") // 弹出:泛型
}

print("弹出后栈大小:\(stringStack.count)") // 弹出后栈大小:2

关键点说明:

  • struct Stack<T>:定义泛型结构体,T 代表任意类型。
  • private var elements: [T]:内部存储数组,类型由 T 决定。
  • mutating:由于结构体是值类型,任何修改都必须用 mutating 声明。
  • pop() 返回 T?,因为可能栈为空,返回可选类型更安全。

这个结构体可以轻松用于 Int、Double、自定义类型等,只需声明 var intStack = Stack<Int>() 即可。

泛型协议:让接口更灵活

协议(Protocol)是 Swift 中实现多态和抽象的核心机制。当你希望协议支持多种类型时,泛型协议就派上用场了。

举个例子:你有一个“比较器”协议,希望它能比较任意可比较类型(如 Int、String、Double)。

protocol ComparableType {
    // 泛型方法:比较两个值
    static func < (lhs: Self, rhs: Self) -> Bool
    static func > (lhs: Self, rhs: Self) -> Bool
}

现在你可以为 Int 和 String 遵循这个协议:

extension Int: ComparableType {}
extension String: ComparableType {}

// 通用比较函数(泛型函数)
func compare<T: ComparableType>(_ a: T, _ b: T) -> String {
    if a < b {
        return "\(a) 小于 \(b)"
    } else if a > b {
        return "\(a) 大于 \(b)"
    } else {
        return "\(a) 等于 \(b)"
    }
}

// 测试
print(compare(10, 5))      // 10 大于 5
print(compare("apple", "banana")) // apple 小于 banana

这里的关键是 T: ComparableType,表示类型 T 必须遵循 ComparableType 协议。Swift 泛型在协议层面的运用,让代码更具扩展性,避免重复实现。

泛型约束:限制类型范围,提升安全性

有时你希望泛型只接受特定类型的子集。比如,只允许数字类型参与运算。这时就要用到泛型约束。

// 泛型函数:计算两个数字的和
func add<T: Numeric>(_ a: T, _ b: T) -> T {
    return a + b
}
  • T: Numeric:这是泛型约束,表示 T 必须遵循 Numeric 协议。
  • Numeric 协议是 Swift 中所有数字类型的父协议,包括 Int、Double、Float 等。

测试:

let sum1 = add(3, 5)           // Int 类型,结果:8
let sum2 = add(3.14, 2.86)     // Double 类型,结果:6.0
let sum3 = add(1.5, 2.5)       // Float 类型,结果:4.0

print(sum1) // 8
print(sum2) // 6.0

没有这个约束,你可能误传字符串或数组进来,编译器会直接报错,防止运行时错误。

你甚至可以同时约束多个协议:

func process<T: Comparable & Equatable>(_ a: T, _ b: T) -> String {
    if a == b {
        return "相等"
    } else if a < b {
        return "小于"
    } else {
        return "大于"
    }
}

这表示 T 必须同时满足 Comparable 和 Equatable 两个协议。

实战:构建一个通用缓存工具

让我们用 Swift 泛型实现一个简单的缓存系统,支持任意类型的数据存储。

class Cache<Key: Hashable, Value> {
    private var storage: [Key: Value] = [:]
    
    // 存储数据
    func set(key: Key, value: Value) {
        storage[key] = value
    }
    
    // 获取数据
    func get(key: Key) -> Value? {
        return storage[key]
    }
    
    // 删除数据
    func remove(key: Key) {
        storage.removeValue(forKey: key)
    }
    
    // 清空缓存
    func clear() {
        storage.removeAll()
    }
    
    // 检查是否包含某个 key
    func contains(key: Key) -> Bool {
        return storage[key] != nil
    }
}

使用示例:

let userCache = Cache<String, String>()
userCache.set(key: "username", value: "alice")
userCache.set(key: "email", value: "alice@example.com")

print(userCache.get(key: "username") ?? "未找到") // alice
print(userCache.contains(key: "email")) // true

userCache.remove(key: "email")
print(userCache.get(key: "email")) // nil

这个缓存系统支持任意 Hashable 类型作为键(如 String、Int、自定义结构体),任意类型作为值。Swift 泛型在这里实现了高度的灵活性与类型安全。

总结:掌握 Swift 泛型,提升代码质量

Swift 泛型是构建高质量、可复用代码的关键工具。它让你的函数、结构体、协议不再局限于单一类型,而是能“通吃”多种数据类型,同时保持类型安全。

从泛型函数到泛型结构体,再到泛型协议与约束,每一步都在帮你写出更优雅、更健壮的代码。当你开始用泛型重构代码时,你会发现:原来那些重复的函数和类,可以被一个通用的版本替代。

更重要的是,Swift 泛型是类型系统的一部分,它在编译期就完成类型检查,避免了运行时的类型错误。这正是 Swift 强类型语言的优雅之处。

对于初学者,建议从泛型函数入手,理解 <T> 的含义;中级开发者可尝试构建泛型容器或协议,提升抽象能力。随着经验积累,你将发现 Swift 泛型不仅是语法特性,更是一种思维方式——用通用解决具体问题。

掌握 Swift 泛型,就是掌握了一种更高阶的编程语言能力。