首页 / Swift 编程语言教程 / 协议 Protocols

Swift 编程语言教程

协议 Protocols

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

SwiftSwift 编程语言教程protocol协议遵循属性要求方法要求协议组合

本节目标:理解协议是”一份要求清单”,学会声明协议、让类型遵循协议,并把它当成一种类型在代码里使用。

你写代码时一定遇到过这种场景:有一批看起来不相干的类型,却都要会做同一件事。比如”能打印自己的信息""能随机出一个数""有面积”。与其给每个类型各写一遍逻辑,不如先约定一份”要求清单”,规定想干这件事的类型必须提供哪些属性和方法。这份清单,就是协议。

1-1 协议是一份要求清单

协议(protocol)本身不实现任何功能,它只定义”蓝图”:哪些属性、哪些方法、哪些初始化器是遵循者必须提供的。类、结构体、枚举都可以去遵循(adopt / conform)一份协议,并在自己的定义里把协议要求的具体实现补上。一旦某个类型满足了协议的全部要求,我们就说它”遵循”了该协议。

打个比方,协议像一张驾照考试的要求表:会起步、会变道、会倒车入库。谁把这几项都练会了,谁就”通过了考试”。至于你开的是轿车还是卡车,考试表并不关心——它只关心你能不能完成那几个动作。Swift 里的协议也是这个思路:它把”能力”从”具体类型”里抽象出来。

Note

因为协议本身也是一种类型,Swift 约定协议名首字母大写,比如 FullyNamedRandomNumberGenerator,这跟 IntString 这些类型名的风格一致。

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 前缀,即使类在实现时可以用 classstatic

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 可以省略。

当子类的指定初始化器同时来自父类重写又要满足协议时,要同时写 requiredoverride

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")
    }
}

即便 CircleAnimal 没有共同父类,只要它们都是类,就能塞进 [AnyObject] 数组。遍历时通过 as? HasArea 把”有面积的”挑出来,类型安全地访问 area。被转换出来的 objectWithArea 在那一刻只被当作 HasArea,所以只能访问协议要求的成员。

1-9 小结

协议是一份”要求清单”,把能力从具体类型里抽出来。它可要求属性(规定可读还是可读写)、方法(含会改自身的 mutating 方法)、初始化器(类实现要 required)。协议本身也是类型,可当泛型约束、可做协议组合、可限制为类专属。用 isas? 能在运行时检查与转换。记住:协议只管”要求什么”,具体怎么实现,交给遵循它的类型。下章我们看怎么给协议加默认实现,把”约定”变成”还能自带行为”。