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

Swift 扩展

扩展(Extension)可以给一个已有的类型添加新功能,而不需要修改它的原始定义。

被扩展的类型可以是结构体、类、枚举,也可以是协议,甚至是 Swift 标准库里的 Int、String 这类内置类型。

本文所有代码基于 Swift 6.x 与 Xcode 16,示例可直接用 swift 命令运行。


扩展能做什么

先看一张能力清单,明确扩展的边界在哪里。

能力是否支持说明
添加计算属性支持不能添加存储属性
添加实例方法、类型方法支持结构体方法需要修改自身时加 mutating
添加构造器支持类的便捷构造器可以,指定构造器不行
添加下标支持读写皆可
添加嵌套类型支持枚举、结构体、类都可以
让类型遵循协议支持最常用的场景之一
添加存储属性不支持会直接编译报错
添加析构器 deinit不支持只能写在类体内部
重写已有功能不支持扩展不是继承

扩展的语法很简单,用关键字 extension 加上类型名,大括号里写新增的成员。

实例

extension 已有类型 {
    // 在这里添加计算属性、方法、构造器、下标、嵌套类型
}

扩展计算属性

扩展可以添加计算属性,但不能添加存储属性,因为存储属性需要额外的内存布局,而扩展不能改变类型已有的内存布局。

下面的例子给 Double 添加了三个单位换算的计算属性。

实例

import Foundation

// 为 Double 扩展计算属性:把千米换算成米、厘米
extension Double {
    var km: Double { self * 1_000.0 }   // 千米
    var m: Double { self }              // 米
    var cm: Double { self / 100.0 }     // 厘米
}

// 为 Int 扩展实例方法:把某个操作重复执行 self 次
extension Int {
    func repetitions(task: () -> Void) {
        for _ in 0..<self {
            task()
        }
    }
}

let distance = 3.0
print("\(distance) 千米 = \(distance.km) 米")
print("\(distance) 千米 = \(distance.cm) 厘米")

3.repetitions {
    print("RUNOOB")
}

运行结果:

3.0 千米 = 3000.0 米
3.0 千米 = 0.03 厘米
RUNOOB
RUNOOB
RUNOOB

计算属性里用到的 self 就是被扩展的那个值本身,所以 3.0.km 等价于 3.0 * 1000.0。


扩展方法

上例中的 repetitions(task:) 就是扩展的实例方法,它接收一个闭包并重复执行。

如果扩展的是结构体或枚举,而方法需要修改实例自身,同样要加 mutating。

实例

import Foundation

struct RunoobCounter {
    var count = 0
}

extension RunoobCounter {
    // 修改自身状态,必须标记 mutating
    mutating func increment() {
        count += 1
    }

    // 只读方法不需要 mutating
    func describe() -> String {
        return "当前计数:\(count)"
    }
}

var counter = RunoobCounter()
counter.increment()
counter.increment()
print(counter.describe())

运行结果:

当前计数:2

注意:只有用 var 声明的结构体实例才能调用 mutating 方法。如果用 let 声明,编译器会报错。


扩展构造器

扩展可以给类型添加新的构造器,方便用另一种方式创建实例。

结构体的成员逐一构造器 init(width:height:) 由编译器自动生成,扩展里可以再补一个「按边长创建正方形」的构造器。

实例

import Foundation

// 结构体本身只有成员逐一构造器 init(width:height:)
struct RunoobSize {
    var width = 0.0
    var height = 0.0
}

// 扩展一个用边长构造正方形的便捷构造器
extension RunoobSize {
    init(side: Double) {
        self.init(width: side, height: side)   // 必须委托给已有的构造器
    }
}

let square = RunoobSize(side: 5.0)
print("width = \(square.width), height = \(square.height)")

运行结果:

width = 5.0, height = 5.0

扩展里的构造器必须保证所有存储属性都被正确初始化,所以这里通过 self.init(...) 委托给已有的构造器完成。

注意:给类写扩展构造器时,只能添加便利构造器,不能添加指定构造器;指定构造器必须写在类体内部。


扩展下标

扩展可以给类型添加下标,让取值语法更自然。

下面的例子给 Int 添加下标,用来取出十进制数从右往左第 n 位的数字。

实例

import Foundation

// 为 Int 扩展下标:取出十进制从右往左第 digitIndex 位的数字
extension Int {
    subscript(digitIndex: Int) -> Int {
        var decimalBase = 1
        for _ in 0..<digitIndex {
            decimalBase *= 10
        }
        return (self / decimalBase) % 10
    }
}

let number = 746381295
print(number[0], number[1], number[2], number[8])

运行结果:

5 9 2 7

number[0] 是个位 5,number[8] 是最高位 7,下标从 0 开始计数。


扩展嵌套类型

扩展可以在已有类型内部添加嵌套类型,常见做法是把某个枚举放进被扩展的类型里,避免污染全局命名空间。

实例

import Foundation

// 扩展嵌套类型
extension Int {
    enum RunoobKind {
        case negative, zero, positive
    }

    var runoobKind: RunoobKind {
        switch self {
        case 0: return .zero
        case let x where x > 0: return .positive
        default: return .negative
        }
    }
}

func describe(_ value: Int) -> String {
    switch value.runoobKind {
    case .negative: return "\(value) 是负数"
    case .zero:     return "\(value) 是零"
    case .positive: return "\(value) 是正数"
    }
}

print(describe(-7))
print(describe(0))
print(describe(42))

运行结果:

-7 是负数
0 是零
42 是正数

枚举 RunoobKind 的完整名字是 Int.RunoobKind,这样命名不会和项目里其他同名的枚举冲突。


扩展实现协议

这是扩展最常用的场景:类型在定义时不遵循协议,之后再通过扩展补上。

这样做的好处是,可以让类型定义保持简洁,把协议实现按功能拆分到多个扩展里。

实例

import Foundation

// 协议要求一个属性与一个方法
protocol RunoobGreetable {
    var name: String { get }
    func greet() -> String
}

// 类型定义时不遵循协议
struct RunoobUser {
    var name: String
}

// 在扩展中遵循协议并实现要求
extension RunoobUser: RunoobGreetable {
    func greet() -> String {
        "Hello, \(name)! Welcome to www.runoob.com"
    }
}

let user = RunoobUser(name: "RUNOOB")
print(user.greet())

// 让已有类型通过扩展获得协议能力
extension String: RunoobGreetable {
    var name: String { self }
    func greet() -> String { "Hello, \(self)!" }
}

print("Runoob".greet())

运行结果:

Hello, RUNOOB! Welcome to www.runoob.com
Hello, Runoob!

String 是标准库类型,我们没有它的源码,但依然能通过扩展让它遵循自定义协议。


扩展泛型类型

泛型类型也可以被扩展。扩展里可以直接使用原有的类型参数,也可以给扩展加上 where 子句,让新功能只在特定条件下可用。

实例

import Foundation

// 泛型结构体:一个简单的栈
struct RunoobStack<Element> {
    private var items: [Element] = []

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        items.popLast()
    }
}

// 普通扩展:对所有 Element 都可用
extension RunoobStack {
    var top: Element? { items.last }
    var isEmpty: Bool { items.isEmpty }
}

// 条件扩展:只有 Element 遵循 Equatable 时才有 contains 方法
extension RunoobStack where Element: Equatable {
    func contains(_ item: Element) -> Bool {
        items.contains(item)
    }
}

var stack = RunoobStack<Int>()
stack.push(10)
stack.push(20)
stack.push(30)

print("栈顶元素:\(stack.top!)")
print("是否包含 20:\(stack.contains(20))")
print("是否包含 99:\(stack.contains(99))")
print("弹出一个元素:\(stack.pop()!)")
print("栈是否为空:\(stack.isEmpty)")

运行结果:

栈顶元素:30
是否包含 20:true
是否包含 99:false
弹出一个元素:30
栈是否为空:false

contains 用到了 == 比较,所以必须约束 Element: Equatable;不满足条件的栈类型上根本不会出现这个方法。


扩展不能添加存储属性与析构器

扩展只负责添加功能,不能改变类型的存储布局,因此不能添加存储属性,也不能添加析构器。

先看添加存储属性的结果:

实例

struct RunoobPoint {
    var x = 0
    var y = 0
}

extension RunoobPoint {
    var runoobTag = "点"   // 扩展中添加存储属性
}

编译报错:

error: extensions must not contain stored properties

再看添加析构器的结果,即使被扩展的是类也一样不行:

实例

class RunoobBox {
    var value = 0
}

extension RunoobBox {
    deinit {
        print("deinit")
    }
}

编译报错:

error: deinitializers may only be declared within a class, actor, or noncopyable type

析构器必须写在类体内部,扩展里无法声明。

注意:扩展也不能「重写」类型已有的方法或属性。如果你在扩展里定义了同名方法,编译器会报重复定义,而不是覆盖。


回溯符合与 @retroactive

回溯符合(retroactive conformance)指的是:在模块 C 里,让来自模块 A 的类型遵循来自模块 B 的协议,而 A、B 都不归你所有。

这种写法本身合法,但存在风险:如果将来 A 的作者自己也让该类型遵循了 B 的协议,两份实现就会冲突。Swift 6 会对此给出警告。

下面用三个文件模拟三个模块。第一个文件是 LibA,只定义一个类型:

实例

// 模块 LibA:定义 RunoobPoint 类型
public struct RunoobPoint {
    public var x: Int
    public init(x: Int) { self.x = x }
}

第二个文件是 LibB,只定义一个协议:

实例

// 模块 LibB:定义 RunoobFormattable 协议
public protocol RunoobFormattable {
    func runoobFormat() -> String
}

第三个文件是 App,在这里写回溯符合。此时 RunoobPoint 与 RunoobFormattable 都是「外部类型」。

实例

import LibA
import LibB

// 在 App 模块中,把来自 LibA 的类型符合到来自 LibB 的协议
extension RunoobPoint: RunoobFormattable {
    public func runoobFormat() -> String { "RunoobPoint(x: \(x))" }
}

用 swiftc 类型检查时会出现警告:

warning: extension declares a conformance of imported type 'RunoobPoint'
to imported protocol 'RunoobFormattable'; this will not behave correctly
if the owners of 'LibA' introduce this conformance in the future
note: add '@retroactive' to silence this warning

按提示加上 @retroactive 即可消除警告,同时明确表达「我知道这个符合是跨模块的、可能冲突」。

实例

import LibA
import LibB

// 用 @retroactive 显式标记回溯符合
extension RunoobPoint: @retroactive RunoobFormattable {
    public func runoobFormat() -> String { "RunoobPoint(x: \(x))" }
}

let point = RunoobPoint(x: 42)
print(point.runoobFormat())

运行结果:

RunoobPoint(x: 42)

@retroactive 只是一个「确认标记」,它不改变行为,只是让编译器不再警告。

注意:如果类型或协议中至少有一个是你自己模块里定义的,就属于普通符合,不需要也不应该加 @retroactive。硬加会得到警告 'retroactive' attribute does not apply。


常见问题

最后集中回答几个初学者常问的问题。

扩展能替代子类吗

不能。扩展给类型增加功能,但改变不了类型之间的继承关系,也不能添加存储属性,所以需要保存新状态时应考虑继承或组合。

扩展能给协议添加默认实现吗

可以。给协议写扩展并实现要求,遵循者就自动获得默认实现,这属于面向协议编程的用法,细节可参考《Swift 协议扩展与面向协议编程》。

给同一个类型写多个扩展有影响吗

没有影响,最终效果等同于把所有成员写在一个扩展里。实践中通常按功能拆分,比如一个扩展管协议实现,一个扩展管格式化方法。

扩展里的 static 成员可以用吗

可以。扩展既能添加实例成员,也能添加 static 或 class 类型成员,规则与写在类型体内一致。

能不能扩展协议本身

可以,这叫协议扩展,用来提供默认实现或条件方法,详见《Swift 协议扩展与面向协议编程》。