协议 Protocols
本教程共 93 篇 · 第 67 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:理解协议是”一份要求清单”,学会声明协议、让类型遵循协议,并把它当成一种类型在代码里使用。
你写代码时一定遇到过这种场景:有一批看起来不相干的类型,却都要会做同一件事。比如”能打印自己的信息""能随机出一个数""有面积”。与其给每个类型各写一遍逻辑,不如先约定一份”要求清单”,规定想干这件事的类型必须提供哪些属性和方法。这份清单,就是协议。
1-1 协议是一份要求清单
协议(protocol)本身不实现任何功能,它只定义”蓝图”:哪些属性、哪些方法、哪些初始化器是遵循者必须提供的。类、结构体、枚举都可以去遵循(adopt / conform)一份协议,并在自己的定义里把协议要求的具体实现补上。一旦某个类型满足了协议的全部要求,我们就说它”遵循”了该协议。
打个比方,协议像一张驾照考试的要求表:会起步、会变道、会倒车入库。谁把这几项都练会了,谁就”通过了考试”。至于你开的是轿车还是卡车,考试表并不关心——它只关心你能不能完成那几个动作。Swift 里的协议也是这个思路:它把”能力”从”具体类型”里抽象出来。
Note因为协议本身也是一种类型,Swift 约定协议名首字母大写,比如
FullyNamed、RandomNumberGenerator,这跟Int、String这些类型名的风格一致。
1-2 协议语法与如何遵循
声明协议用 protocol 关键字,写法和类、结构体、枚举很像:
protocol SomeProtocol {
// 协议定义写在这里
}
要让一个类型遵循协议,把协议名写在类型名后面,用冒号隔开。如果要遵循多个协议,用逗号分隔:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 结构体定义
}
如果是一个类,而且还有父类,要把父类写在最前面,再跟协议:
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// 类定义
}
初学者常在这里踩坑:只写了协议要求的方法,却忘在类型声明处写”: 协议名”。Swift 不会因为你”碰巧”实现了同名方法就自动算你遵循了协议——必须显式声明。少写冒号后面那一段,编译器会直接报”没遵循协议”。
1-3 属性要求
协议可以要求遵循者提供某个特定名字和类型的属性。它不规定这个属性是存储属性还是计算属性,只规定名字和类型,以及它是”只读”还是”可读可写”。
属性要求一律用 var 声明。只读写成 { get },可读可写写成 { get set }:
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
这里有条铁律:如果协议要求”可读可写”,那遵循者不能用常量存储属性(只读),也不能用只读计算属性去满足;如果协议只要求”可读”,那遵循者用任何形式的属性都行,甚至额外加上可写也可以。
类型属性(type property)要求,在协议里始终用 static 前缀,即使类在实现时可以用 class 或 static:
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
下面这个协议只要求一个可读取的 fullName 字符串属性:
protocol FullyNamed {
var fullName: String { get }
}
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
只要 Person 提供了名为 fullName、类型为 String 且可读取的属性,就满足了 FullyNamed。类也可以满足它,而且可以把这个属性实现成计算属性:
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
return (prefix != nil ? prefix! + " " : "") + name
}
}
1-4 方法要求
协议可以要求遵循者实现某些实例方法或类型方法。写法跟普通方法一样,但去掉大括号和方法体。可变参数允许使用,但不能给参数设默认值。类型方法要求同样用 static 前缀。
protocol RandomNumberGenerator {
func random() -> Double
}
这份协议只说:遵循者得有个 random() 方法,调用时返回一个 Double。它完全不关心随机数怎么算出来的。下面这个类用一个经典算法实现了它:
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0
let m = 139968.0
let a = 3877.0
let c = 29573.0
func random() -> Double {
lastRandom = ((lastRandom * a + c)
.truncatingRemainder(dividingBy: m))
return lastRandom / m
}
}
let generator = LinearCongruentialGenerator()
print(generator.random())
1-5 会修改自身的方法要标 mutating
如果一个方法会改动它所属实例的状态(典型如结构体、枚举这类值类型里的方法),在协议里要把它标成 mutating。这样结构体和枚举才能去遵循这个协议。
protocol Togglable {
mutating func toggle()
}
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off: self = .on
case .on: self = .off
}
}
}
注意:类在实现协议方法时,不用写 mutating——这个关键字只服务于值类型。换句话说,协议写 mutating 是为了”放行”结构体和枚举,类那边可以忽略它。
1-6 初始化器要求
协议可以要求遵循者实现特定初始化器,写法跟普通初始化器一样,但去掉大括号和函数体:
protocol SomeProtocol {
init(someParameter: Int)
}
如果一个类是遵循者,它必须把该初始化器实现成”指定初始化器”或”便利初始化器”,而且都要加 required 修饰符——这是为了确保它的所有子类也都有这个初始化器,从而继续遵循协议。不过,被 final 标记的类不能再加子类,所以 required 可以省略。
当子类的指定初始化器同时来自父类重写又要满足协议时,要同时写 required 和 override:
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
required override init() {}
}
1-7 协议可以当成类型用
协议本身虽然不干活,但你可以把它当作一种类型来用。最常见的用法是当作泛型约束:函数或类型只要写”接受任意遵循某协议的类型”,调用方再决定具体传哪个类型。
另一种常见用法是协议组合(protocol composition),一次要求”同时遵循多个协议”,写法是用 & 连接:
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
Named & Aged 的意思是”任何同时遵循这两个协议的类型”。它并不限定具体的类型名,只要满足组合要求即可。
有些协议只希望被类遵循(因为要用 weak 弱引用、依赖引用语义),那就让它继承 AnyObject:
protocol SomeClassOnlyProtocol: AnyObject {
// 只能被类遵循
}
对值类型(结构体、枚举)来说,遵循这种协议是编译错误。
1-8 检查类型是否遵循协议
你可以用 is 判断某个实例是否遵循协议,用 as? 尝试转换、失败返回 nil,用 as! 强制转换(失败会运行时报错):
protocol HasArea {
var area: Double { get }
}
class Circle: HasArea {
let pi = 3.1415927
var radius: Double
var area: Double { return pi * radius * radius }
init(radius: Double) { self.radius = radius }
}
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
let objects: [AnyObject] = [Circle(radius: 2.0), Animal(legs: 4)]
for object in objects {
if let objectWithArea = object as? HasArea {
print("Area is \(objectWithArea.area)")
} else {
print("Something that doesn't have an area")
}
}
即便 Circle 和 Animal 没有共同父类,只要它们都是类,就能塞进 [AnyObject] 数组。遍历时通过 as? HasArea 把”有面积的”挑出来,类型安全地访问 area。被转换出来的 objectWithArea 在那一刻只被当作 HasArea,所以只能访问协议要求的成员。
1-9 小结
协议是一份”要求清单”,把能力从具体类型里抽出来。它可要求属性(规定可读还是可读写)、方法(含会改自身的 mutating 方法)、初始化器(类实现要 required)。协议本身也是类型,可当泛型约束、可做协议组合、可限制为类专属。用 is 和 as? 能在运行时检查与转换。记住:协议只管”要求什么”,具体怎么实现,交给遵循它的类型。下章我们看怎么给协议加默认实现,把”约定”变成”还能自带行为”。