Swift 協(xié)議

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é)議的使用可以讓你的代碼更加靈活、可測試和可擴展。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 協(xié)議 協(xié)議 定義了一個藍(lán)圖,規(guī)定了用來實現(xiàn)某一特定任務(wù)或者功能的方法、屬性,以及其他需要的東西。類、結(jié)構(gòu)體或枚舉都...
    xiaofu666閱讀 846評論 0 0
  • 1、協(xié)議 1、協(xié)議可以用來定義方法、屬性、下標(biāo)的聲明,協(xié)議可以被枚舉、結(jié)構(gòu)體、類遵守(多個協(xié)議之間用逗號隔開)。 ...
    Abner_XuanYuan閱讀 109評論 0 0
  • 協(xié)議語法 與類、結(jié)構(gòu)體、枚舉類型非常相似 有父類的寫法,父類名放協(xié)議名之前,用逗號分隔 屬性要求 場景:要求遵循該...
    Sunday_David閱讀 427評論 0 0
  • 掌握Swift協(xié)議 用日常術(shù)語來說,我們談?wù)摰膮f(xié)議是指用于控制事件的設(shè)置過程或規(guī)則系統(tǒng)。每當(dāng)您啟動一個event時...
    iCloudEnd閱讀 1,178評論 0 1
  • 1.協(xié)議的語法 定義協(xié)議: 遵守協(xié)議: 當(dāng)一個類既有父類,又遵守其他協(xié)議時,將父類名寫在所遵守協(xié)議的前面: 2.屬...
    WSJay閱讀 27,457評論 3 18

友情鏈接更多精彩內(nèi)容