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

Swift 协议

协议(Protocol)是一份「能力清单」,只规定需要提供哪些属性、方法和构造器,不关心具体怎么实现。

类、结构体和枚举都可以遵循协议,这让完全不同类型的对象可以被同一段代码处理。

Swift 是面向协议编程的语言,标准库里的 Sequence、Collection、Equatable、Codable 都是协议。


协议定义与遵循

protocol 定义协议,遵循时把协议名写在类型名后面,用冒号分隔。

一个类型可以同时遵循多个协议,协议之间用逗号分隔。

实例

import Foundation

// 协议只描述"需要什么",不提供实现
protocol RunoobDescribable {
    var name: String { get }
    func describe() -> String
}

struct RunoobSite: RunoobDescribable {
    var name: String
    func describe() -> String {
        return "站点:\(name)"
    }
}

struct RunoobUser: RunoobDescribable {
    var name: String
    func describe() -> String {
        return "用户:\(name)"
    }
}

let items: [any RunoobDescribable] = [
    RunoobSite(name: "www.runoob.com"),
    RunoobUser(name: "RUNOOB")
]
for item in items {
    print(item.describe())
}

运行结果:

站点:www.runoob.com
用户:RUNOOB

数组的元素类型写成 any RunoobDescribable,表示「任何遵循这个协议的类型」,两个结构体因此能放进同一个数组。

遍历时调用 describe(),实际执行的是各自类型的实现,这就是动态派发。


属性要求

协议里的属性只写名字和类型,后面用 { get }{ get set } 说明可读写性。

协议不管它是存储属性还是计算属性,只看它能不能满足读写要求。

实例

import Foundation

protocol RunoobNamed {
    var name: String { get }             // 只读要求
    var displayName: String { get set }  // 可读写要求
}

struct RunoobTag: RunoobNamed {
    let name: String                     // let 存储属性即可满足 get 要求
    var displayName: String
}

class RunoobCategory: RunoobNamed {
    var name: String
    var displayName: String {
        didSet { print("displayName 改为 \(displayName)") }
    }
    init(name: String, displayName: String) {
        self.name = name
        self.displayName = displayName
    }
}

let tag = RunoobTag(name: "swift", displayName: "Swift 教程")
print(tag.displayName)

let category = RunoobCategory(name: "swift", displayName: "Swift")
category.displayName = "Swift 6"

运行结果:

Swift 教程
displayName 改为 Swift 6

只读要求 { get } 最宽松:let 存储属性、只读计算属性、可读写属性都能满足它。

可读写要求 { get set } 则必须提供 get 和 set,所以 let 常量满足不了。

注意:协议里的 { get set } 描述的是「遵循方需要提供的接口」,不是「协议自己有一个可读写的属性」。协议本身不存储任何值。


方法要求与 mutating

方法要求在协议里只写签名,不写大括号和方法体。

参数标签规则和普通函数一样:所有参数默认都带外部标签,想省略要显式写 _

如果方法要修改遵循者的自身状态,协议里要标记 mutating

实例

import Foundation

protocol RunoobCounter {
    var count: Int { get }
    mutating func increment()   // 值类型遵循时,实现必须标记 mutating
}

struct RunoobTally: RunoobCounter {
    var count = 0
    mutating func increment() {
        count += 1
    }
}

class RunoobSharedCounter: RunoobCounter {
    var count = 0
    func increment() {          // 类遵循时不需要 mutating
        count += 1
    }
}

var tally = RunoobTally()
tally.increment()
tally.increment()
print("结构体计数:\(tally.count)")

let shared = RunoobSharedCounter()
shared.increment()
print("类计数:\(shared.count)")

运行结果:

结构体计数:2
类计数:1

结构体的 increment() 必须写成 mutating,因为它修改了自身的存储属性。

类的方法本来就是引用语义,修改属性不需要 mutating,所以协议里的 mutating 要求对类不生效。

注意:如果用一个 let 声明的结构体常量调用 mutating 方法,会编译报错,因为常量不允许被修改。


构造器要求与 required

协议可以要求遵循方实现某个构造器,写法是 init(参数),同样不写方法体。

类遵循带构造器要求的协议时,必须给构造器加上 required,子类才被强制实现它。

实例

import Foundation

protocol RunoobInitable {
    init(name: String)
}

class RunoobNode: RunoobInitable {
    var name: String
    required init(name: String) {   // 类遵循带构造器要求的协议时必须写 required
        self.name = name
    }
    func describe() -> String { "节点 \(name)" }
}

class RunoobLeaf: RunoobNode {
    var level: Int

    init(name: String, level: Int) {
        self.level = level
        super.init(name: name)
    }

    // 子类定义了自己的指定构造器,不会自动继承父类构造器,因此要重新实现 required init
    required init(name: String) {
        self.level = 1
        super.init(name: name)
    }

    override func describe() -> String { "叶子节点 \(name)(第 \(level) 层)" }
}

let leaf: any RunoobInitable = RunoobLeaf(name: "runoob", level: 2)
if let node = leaf as? RunoobLeaf {
    print(node.describe())
}

运行结果:

叶子节点 runoob(第 2 层)

RunoobNode 直接遵循协议,所以它的 init(name:) 必须标 required。

RunoobLeaf 定义了自己的指定构造器,因此不再自动继承父类构造器,需要自己再实现一遍 required init。

如果子类没有定义任何指定构造器,它可以直接继承父类那个 required init,就不用重复写。


协议作为类型与 any Protocol

协议可以当作类型使用:声明变量、数组元素、函数参数和返回值都可以写协议名。

从 Swift 5.7 起,官方推荐在协议类型前显式写 any,表示这是一个存在类型(existential type)。

它的含义是「盒子里装着某个未知的具体类型,只知道它遵循这个协议」。

实例

import Foundation

protocol RunoobLogger {
    func log(_ message: String)
}

struct RunoobConsoleLogger: RunoobLogger {
    func log(_ message: String) {
        print("[控制台] \(message)")
    }
}

struct RunoobFileLogger: RunoobLogger {
    var fileName: String
    func log(_ message: String) {
        print("[\(fileName)] \(message)")
    }
}

// 协议作为类型:数组元素可以是任何遵循 RunoobLogger 的类型
var loggers: [any RunoobLogger] = [
    RunoobConsoleLogger(),
    RunoobFileLogger(fileName: "runoob.log")
]
loggers.append(RunoobConsoleLogger())

for logger in loggers {
    logger.log("来自 www.runoob.com 的日志")
}

let single: any RunoobLogger = RunoobFileLogger(fileName: "www.runoob.com.log")
single.log("RUNOOB")

运行结果:

[控制台] 来自 www.runoob.com 的日志
[runoob.log] 来自 www.runoob.com 的日志
[控制台] 来自 www.runoob.com 的日志
[www.runoob.com.log] RUNOOB

any 类型的变量只能调用协议里声明过的成员,访问不到具体类型的其他属性。

如果需要用到具体类型的能力,可以先用 as? 向下转型,见《Swift 类型转换》。

注意:不要把所有协议参数都写成 any P。如果函数只需要调用协议成员,用泛型 func f<T: P>(_ value: T) 可以让编译器做静态派发,性能更好。两者的区别见《Swift some 与 any》。


协议继承

一个协议可以继承另一个协议,从而把多个要求合并成一份更大的契约。

下面的 RunoobPerson 同时要求 name、age 和 greet(),遵循它就等于同时遵循了另外两个协议。

实例

import Foundation

protocol RunoobNamed2 {
    var name: String { get }
}

protocol RunoobAged {
    var age: Int { get }
}

// 协议继承:RunoobPerson 同时要求 name、age 和 greet()
protocol RunoobPerson: RunoobNamed2, RunoobAged {
    func greet()
}

struct RunoobStudent: RunoobPerson {
    var name: String
    var age: Int
    func greet() {
        print("我是 \(name),今年 \(age) 岁")
    }
}

// 协议组合:A & B 表示同时满足两个协议
func showInfo(_ value: any RunoobNamed2 & RunoobAged) {
    print("\(value.name) / \(value.age)")
}

let student = RunoobStudent(name: "小明", age: 18)
student.greet()
showInfo(student)

运行结果:

我是 小明,今年 18 岁
小明 / 18

协议继承的写法是冒号后跟多个协议名,和类继承的语法看起来一样,但协议可以继承多个。

协议不能继承类,只有类才能被类继承。


协议组合

协议组合用 & 把多个协议拼成一个临时类型,表示「同时满足这几个协议」。

它常用于函数参数:不想为每种组合都新建一个协议时,直接写组合即可。

上面的 showInfo 已经演示了 any RunoobNamed2 & RunoobAged,它接受的参数必须同时有 name 和 age。

协议组合里也可以包含一个类名,表示「必须是这个类的子类,同时遵循这些协议」。


类专属协议 AnyObject

如果协议只允许类遵循,让它继承 AnyObject 即可。

这样做的好处是协议类型可以安全地做引用比较、弱引用,因为它一定是引用类型。

实例

import Foundation

// 类专属协议:只有类可以遵循,结构体和枚举都不行
protocol RunoobClassOnly: AnyObject {
    var identifier: String { get set }
}

class RunoobViewModel: RunoobClassOnly {
    var identifier: String
    init(identifier: String) { self.identifier = identifier }
}

var vm: any RunoobClassOnly = RunoobViewModel(identifier: "home")
let other = RunoobViewModel(identifier: "runoob")
print(vm === other)
vm = other
print(vm.identifier)
print(vm === other)

运行结果:

false
runoob
true

因为协议限定了 AnyObject,编译器知道 vm 一定是类的实例,所以允许直接用 === 比较身份。

注意:类专属协议的正确写法是 protocol P: AnyObject。旧教程里常见的让协议继承 class 的写法属于 Swift 早期语法,现在已经是错误写法,不要再用。


常见问题

下面整理了协议使用中常见的几个疑问。

协议能给成员提供默认实现吗?

能,但要写在协议扩展里,而不是协议本体中。

这部分内容单独成篇,见《Swift 协议扩展与面向协议编程》。

协议可以包含存储属性吗?

不可以。协议只能声明属性的读写要求,不能声明存储属性,也不能有属性默认值。

遵循协议的类型自己决定用存储属性还是计算属性来满足要求。

结构体遵循带 mutating 要求的协议时,能改成普通方法吗?

不能。协议里写了 mutating,值类型实现时就必须也写 mutating,否则编译报错。

类不受这条限制,类的方法不带 mutating 也能满足要求。

一个类型能遵循多少个协议?

没有数量限制,用逗号把协议名逐个列出即可。

如果多个协议有同名成员,调用时可能需要显式指定用哪个协议的版本。

协议和抽象类有什么区别?

抽象类只能被类继承,而协议可以被类、结构体和枚举遵循,适用面更广。

Swift 没有专门的 abstract 关键字,需要共享实现时通常用协议加协议扩展来替代。