Swift 協(xié)議(Protocol)完全指南:從入門到精通
協(xié)議是 Swift 最核心的特性之一,也是面向協(xié)議編程(POP)的基礎(chǔ)。下面我從簡單到復(fù)雜,帶你完整掌握協(xié)議的使用。
一、協(xié)議基礎(chǔ)(入門級)
1.1 最簡單的協(xié)議
// 定義一個協(xié)議:可描述的東西
protocol Describable {
var description: String { get } // 只讀屬性
func describe() -> String // 方法
}
// 遵守協(xié)議的結(jié)構(gòu)體
struct Person: Describable {
var name: String
var age: Int
var description: String {
return "\(name), \(age)歲"
}
func describe() -> String {
return "我是\(name),今年\(age)歲"
}
}
1.2 協(xié)議的屬性要求
protocol Vehicle {
// 屬性要求:可讀可寫
var brand: String { get set }
// 只讀屬性
var year: Int { get }
// 靜態(tài)屬性
static var wheels: Int { get }
}
struct Car: Vehicle {
var brand: String
var year: Int
static let wheels = 4 // 所有 Car 都是 4 個輪子
}
struct Motorcycle: Vehicle {
var brand: String
var year: Int
static let wheels = 2
}
二、方法協(xié)議(進階級)
2.1 實例方法和靜態(tài)方法
protocol Calculator {
// 實例方法
func add(_ a: Int, _ b: Int) -> Int
func subtract(_ a: Int, _ b: Int) -> Int
// 靜態(tài)方法
static func version() -> String
// mutating 方法(值類型需要修改自身)
mutating func reset()
}
struct BasicCalculator: Calculator {
var result = 0
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
func subtract(_ a: Int, _ b: Int) -> Int {
return a - b
}
static func version() -> String {
return "1.0.0"
}
mutating func reset() {
result = 0
}
}
2.2 協(xié)議中的初始化器
protocol JSONInitializable {
init(json: [String: Any])
init?(dictionary: [String: Any]?) // 可失敗的初始化器
}
struct User: JSONInitializable {
let name: String
let age: Int
init(json: [String: Any]) {
name = json["name"] as? String ?? ""
age = json["age"] as? Int ?? 0
}
init?(dictionary: [String: Any]?) {
guard let dict = dictionary,
let name = dict["name"] as? String,
let age = dict["age"] as? Int else {
return nil
}
self.name = name
self.age = age
}
}
三、協(xié)議組合(中等級)
3.1 繼承多個協(xié)議
protocol Nameable {
var name: String { get }
}
protocol Agable {
var age: Int { get }
}
protocol Emailable {
var email: String { get }
}
// 組合多個協(xié)議
protocol UserType: Nameable, Agable, Emailable { }
struct Employee: UserType {
let name: String
let age: Int
let email: String
let position: String // 額外屬性
}
3.2 類型約束和協(xié)議組合
// 要求參數(shù)同時遵守多個協(xié)議
func processUser<T: Nameable & Agable>(_ user: T) {
print("用戶 \(user.name) 年齡 \(user.age)")
}
// 使用 & 符號組合協(xié)議
func displayInfo(_ item: Nameable & Emailable) {
print("\(item.name) 聯(lián)系: \(item.email)")
}
四、協(xié)議擴展(高級)
4.1 為協(xié)議提供默認(rèn)實現(xiàn)
protocol Animal {
var name: String { get }
func makeSound()
}
// 協(xié)議擴展:提供默認(rèn)實現(xiàn)
extension Animal {
func makeSound() {
print("\(name) 發(fā)出聲音")
}
// 非協(xié)議要求的擴展方法
func breathe() {
print("\(name) 在呼吸")
}
}
struct Dog: Animal {
let name: String
// 可以不實現(xiàn) makeSound,使用默認(rèn)實現(xiàn)
}
struct Cat: Animal {
let name: String
// 可以選擇重寫
func makeSound() {
print("\(name): 喵喵喵")
}
}
let dog = Dog(name: "旺財")
dog.makeSound() // "旺財 發(fā)出聲音"(使用默認(rèn)實現(xiàn))
dog.breathe() // "旺財 在呼吸"
let cat = Cat(name: "咪咪")
cat.makeSound() // "咪咪: 喵喵喵"(使用自定義實現(xiàn))
4.2 約束協(xié)議擴展(where 子句)
protocol Container {
associatedtype Item
var count: Int { get }
func add(_ item: Item)
func get(at index: Int) -> Item?
}
// 當(dāng) Item 是 Int 時的特殊擴展
extension Container where Item == Int {
func sum() -> Int {
var total = 0
for i in 0..<count {
total += get(at: i) ?? 0
}
return total
}
}
// 當(dāng) Item 是 String 時的特殊擴展
extension Container where Item == String {
func concatenate(separator: String = "") -> String {
var result = ""
for i in 0..<count {
result += (get(at: i) ?? "")
if i < count - 1 {
result += separator
}
}
return result
}
}
五、關(guān)聯(lián)類型(高級)
5.1 使用 associatedtype
// 協(xié)議中的泛型
protocol DataProvider {
associatedtype DataType
associatedtype ErrorType: Error
func fetch() async throws -> DataType
var cache: DataType? { get set }
}
// 實現(xiàn)協(xié)議時必須指定具體類型
struct UserProvider: DataProvider {
typealias DataType = [User]
typealias ErrorType = NetworkError
var cache: [User]?
func fetch() async throws -> [User] {
// 實現(xiàn)網(wǎng)絡(luò)請求
return []
}
}
enum NetworkError: Error {
case noConnection
case timeout
}
// 泛型約束關(guān)聯(lián)類型
func process<T: DataProvider>(_ provider: T) where T.DataType: Collection {
print("處理集合類型數(shù)據(jù)")
}
5.2 通過協(xié)議消除關(guān)聯(lián)類型
// 使用 AnyDataProvider 消除關(guān)聯(lián)類型
struct AnyDataProvider<DataType>: DataProvider {
typealias ErrorType = Error
private let _fetch: () async throws -> DataType
init<P: DataProvider>(_ provider: P) where P.DataType == DataType {
_fetch = provider.fetch
}
func fetch() async throws -> DataType {
try await _fetch()
}
var cache: DataType?
}
六、完整實戰(zhàn) Demo
Demo 1: 可繪制圖形的協(xié)議系統(tǒng)
import UIKit
// 基礎(chǔ)協(xié)議
protocol Drawable {
func draw() -> UIImage
var boundingRect: CGRect { get }
}
protocol Colorable {
var fillColor: UIColor { get set }
var strokeColor: UIColor { get set }
var lineWidth: CGFloat { get set }
}
protocol Transformable {
func translated(by point: CGPoint) -> Self
func rotated(by angle: CGFloat) -> Self
func scaled(by factor: CGFloat) -> Self
}
// 協(xié)議組合
typealias StylableDrawable = Drawable & Colorable
// 圓形
struct Circle: Drawable, Colorable, Transformable {
var center: CGPoint
var radius: CGFloat
var fillColor: UIColor = .red
var strokeColor: UIColor = .black
var lineWidth: CGFloat = 2
func draw() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: boundingRect.size)
return renderer.image { context in
let rect = boundingRect
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY),
radius: radius,
startAngle: 0,
endAngle: 2 * .pi,
clockwise: true)
fillColor.setFill()
strokeColor.setStroke()
path.lineWidth = lineWidth
path.fill()
path.stroke()
}
}
var boundingRect: CGRect {
return CGRect(x: center.x - radius,
y: center.y - radius,
width: radius * 2,
height: radius * 2)
}
func translated(by point: CGPoint) -> Circle {
var copy = self
copy.center = CGPoint(x: center.x + point.x,
y: center.y + point.y)
return copy
}
func rotated(by angle: CGFloat) -> Circle {
return self // 圓形旋轉(zhuǎn)不變
}
func scaled(by factor: CGFloat) -> Circle {
var copy = self
copy.radius *= factor
return copy
}
}
// 矩形
struct Rectangle: Drawable, Colorable, Transformable {
var origin: CGPoint
var size: CGSize
var fillColor: UIColor = .blue
var strokeColor: UIColor = .black
var lineWidth: CGFloat = 2
func draw() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: boundingRect.size)
return renderer.image { context in
let path = UIBezierPath(rect: boundingRect)
fillColor.setFill()
strokeColor.setStroke()
path.lineWidth = lineWidth
path.fill()
path.stroke()
}
}
var boundingRect: CGRect {
return CGRect(origin: origin, size: size)
}
func translated(by point: CGPoint) -> Rectangle {
var copy = self
copy.origin = CGPoint(x: origin.x + point.x,
y: origin.y + point.y)
return copy
}
func rotated(by angle: CGFloat) -> Rectangle {
// 簡化實現(xiàn),實際需要矩陣變換
return self
}
func scaled(by factor: CGFloat) -> Rectangle {
var copy = self
copy.size = CGSize(width: size.width * factor,
height: size.height * factor)
return copy
}
}
// 畫板:可以繪制任何 Drawable 對象
class DrawingBoard {
private var shapes: [Drawable] = []
func addShape<T: Drawable>(_ shape: T) {
shapes.append(shape)
}
func render() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 800, height: 600))
return renderer.image { _ in
for shape in shapes {
let image = shape.draw()
image.draw(at: shape.boundingRect.origin)
}
}
}
// 使用協(xié)議組合作為參數(shù)
func applyStyle(to shape: StylableDrawable) -> StylableDrawable {
var styled = shape
styled.fillColor = .green
styled.strokeColor = .white
styled.lineWidth = 3
return styled
}
}
// 使用示例
func demoDrawable() {
let board = DrawingBoard()
let circle = Circle(center: CGPoint(x: 100, y: 100),
radius: 50)
let rectangle = Rectangle(origin: CGPoint(x: 200, y: 200),
size: CGSize(width: 80, height: 60))
board.addShape(circle)
board.addShape(rectangle)
let finalImage = board.render()
print("繪制完成,圖片尺寸: \(finalImage.size)")
}
Demo 2: 可緩存的數(shù)據(jù)協(xié)議
import Foundation
// 可緩存協(xié)議
protocol Cacheable {
associatedtype Key: Hashable
associatedtype Value
func cache(_ value: Value, for key: Key)
func get(for key: Key) -> Value?
func remove(for key: Key)
}
// 內(nèi)存緩存實現(xiàn)
class MemoryCache<Key: Hashable, Value>: Cacheable {
private var storage: [Key: Value] = [:]
private let queue = DispatchQueue(label: "cache.queue", attributes: .concurrent)
func cache(_ value: Value, for key: Key) {
queue.async(flags: .barrier) {
self.storage[key] = value
}
}
func get(for key: Key) -> Value? {
return queue.sync {
return storage[key]
}
}
func remove(for key: Key) {
queue.async(flags: .barrier) {
self.storage.removeValue(forKey: key)
}
}
}
// 磁盤緩存實現(xiàn)
class DiskCache<Key: Hashable, Value: Codable>: Cacheable {
private let cacheDirectory: URL
init(cacheName: String) {
let urls = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
cacheDirectory = urls[0].appendingPathComponent(cacheName)
try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
}
private func fileURL(for key: Key) -> URL {
let keyString = String(describing: key)
return cacheDirectory.appendingPathComponent("\(keyString).cache")
}
func cache(_ value: Value, for key: Key) {
let url = fileURL(for: key)
let encoder = JSONEncoder()
guard let data = try? encoder.encode(value) else { return }
try? data.write(to: url)
}
func get(for key: Key) -> Value? {
let url = fileURL(for: key)
guard let data = try? Data(contentsOf: url) else { return nil }
let decoder = JSONDecoder()
return try? decoder.decode(Value.self, from: data)
}
func remove(for key: Key) {
let url = fileURL(for: key)
try? FileManager.default.removeItem(at: url)
}
}
// 使用協(xié)議作為泛型約束
class CacheManager<Key: Hashable, Value> {
private let cache: any Cacheable<Key, Value>
init(cache: any Cacheable<Key, Value>) {
self.cache = cache
}
func save(_ value: Value, key: Key) {
cache.cache(value, for: key)
}
func load(key: Key) -> Value? {
return cache.get(for: key)
}
}
// 使用示例
struct CachedUser: Codable {
let id: Int
let name: String
}
func demoCache() {
let memoryCache = MemoryCache<Int, CachedUser>()
let diskCache = DiskCache<Int, CachedUser>(cacheName: "userCache")
let manager = CacheManager(cache: memoryCache)
let user = CachedUser(id: 1, name: "Alice")
manager.save(user, key: 1)
let loaded = manager.load(key: 1)
print("加載用戶: \(loaded?.name ?? "nil")")
}
Demo 3: 可比較和排序的協(xié)議
// 自定義排序協(xié)議
protocol Rankable {
associatedtype RankValue: Comparable
var rank: RankValue { get }
}
extension Array where Element: Rankable {
func sortedByRank() -> [Element] {
return sorted { $0.rank < $1.rank }
}
func top(_ count: Int) -> [Element] {
return sortedByRank().prefix(count).map { $0 }
}
}
// 學(xué)生成績
struct Student: Rankable {
let name: String
let score: Double
typealias RankValue = Double
var rank: Double { score }
}
// 商品銷量
struct Product: Rankable {
let name: String
let sales: Int
typealias RankValue = Int
var rank: Int { sales }
}
// 使用示例
func demoRankable() {
let students = [
Student(name: "張三", score: 85),
Student(name: "李四", score: 92),
Student(name: "王五", score: 78),
Student(name: "趙六", score: 95)
]
let topStudents = students.top(2)
print("前兩名: \(topStudents.map { $0.name })")
let products = [
Product(name: "iPhone", sales: 1000),
Product(name: "iPad", sales: 800),
Product(name: "Mac", sales: 600)
]
let sortedProducts = products.sortedByRank()
print("銷量排名: \(sortedProducts.map { $0.name })")
}
七、協(xié)議使用場景總結(jié)
| 場景 | 協(xié)議使用方式 | 示例 |
|---|---|---|
| 代碼復(fù)用 | 協(xié)議擴展提供默認(rèn)實現(xiàn) |
Animal 協(xié)議的默認(rèn) makeSound()
|
| 依賴注入 | 協(xié)議作為接口抽象 |
Cacheable 協(xié)議,多種緩存實現(xiàn) |
| 回調(diào)/委托 | 委托協(xié)議 | UITableViewDelegate |
| 數(shù)據(jù)源 | 數(shù)據(jù)源協(xié)議 | UITableViewDataSource |
| 類型約束 | 協(xié)議組合 | Nameable & Agable |
| 插件架構(gòu) | 協(xié)議定義擴展點 |
Drawable 協(xié)議支持任意圖形 |
八、最佳實踐建議
// ? 推薦:面向協(xié)議編程
protocol Database {
func save(_ data: Data) async throws
func load(id: String) async throws -> Data
}
// ? 推薦:使用協(xié)議擴展提供共享功能
extension Database {
func saveMultiple(_ items: [Data]) async throws {
for item in items {
try await save(item)
}
}
}
// ? 推薦:使用 where 子句精確約束
extension Collection where Element: Comparable {
func findAllDuplicates() -> [Element] {
// 只有當(dāng)元素可比較時才能找到重復(fù)項
var result: [Element] = []
for item in self {
// 查找邏輯
}
return result
}
}
// ? 避免:過度使用關(guān)聯(lián)類型(當(dāng)不需要時)
protocol Overkill {
associatedtype T
associatedtype U
associatedtype V
// 太多的關(guān)聯(lián)類型會讓協(xié)議難以使用
}
// ? 推薦:保持協(xié)議簡潔
protocol Focused {
var id: String { get }
func process()
}
協(xié)議是 Swift 最強大的特性之一,掌握好協(xié)議的使用可以讓你的代碼更加靈活、可測試和可擴展。