MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift协议与扩展的灵活应用

2023-10-305.5k 阅读

Swift 协议(Protocols)基础

协议定义与基本使用

在 Swift 中,协议是一种定义了方法、属性和其他需求的蓝图,但并不提供这些需求的具体实现。其他类型可以通过遵循(conform)协议来表明它们满足协议中定义的要求。

协议的定义使用 protocol 关键字,如下是一个简单的协议定义示例:

protocol Identifiable {
    var id: String { get }
}

在上述协议 Identifiable 中,定义了一个只读属性 id,类型为 String。任何遵循 Identifiable 协议的类型都必须提供 id 属性的实现。

类、结构体和枚举都可以遵循协议。例如,定义一个遵循 Identifiable 协议的结构体:

struct User: Identifiable {
    var id: String
    var name: String
}
let user = User(id: "123", name: "John")
print(user.id)

这里 User 结构体遵循了 Identifiable 协议,并提供了 id 属性的实现。

协议中的方法要求

协议不仅可以定义属性要求,还能定义方法要求。方法要求可以是实例方法或类方法。下面是一个包含实例方法要求的协议示例:

protocol Runner {
    func run()
}

任何遵循 Runner 协议的类型都必须实现 run 方法。比如,定义一个遵循 Runner 协议的类:

class Athlete: Runner {
    func run() {
        print("The athlete is running.")
    }
}
let athlete = Athlete()
athlete.run()

Athlete 类实现了 Runner 协议中的 run 方法。

对于类方法要求,协议定义如下:

protocol Loggable {
    static func log(message: String)
}

遵循 Loggable 协议的类型必须实现类方法 log,例如:

class Logger: Loggable {
    static func log(message: String) {
        print("Log: \(message)")
    }
}
Logger.log(message: "This is a log message.")

协议中的初始化器要求

协议可以定义初始化器要求,要求遵循协议的类型必须提供特定的初始化器。

protocol Initializable {
    init(data: String)
}

类在遵循包含初始化器要求的协议时,需要注意以下几点。如果类是 final 的,直接实现初始化器即可:

final class FinalClass: Initializable {
    var data: String
    init(data: String) {
        self.data = data
    }
}

如果类不是 final 的,在实现协议初始化器时,需要使用 required 关键字:

class NonFinalClass: Initializable {
    var data: String
    required init(data: String) {
        self.data = data
    }
}

required 关键字确保所有子类也必须实现这个初始化器,以满足协议要求。

协议的继承与组合

协议继承

协议可以继承自一个或多个其他协议,继承后会获得父协议的所有要求。例如:

protocol Payable {
    func pay(amount: Double)
}
protocol Discountable {
    func applyDiscount() -> Double
}
protocol Purchasable: Payable, Discountable {
    var price: Double { get }
}

Purchasable 协议继承自 PayableDiscountable 协议,并添加了 price 属性要求。任何遵循 Purchasable 协议的类型都必须实现 payapplyDiscount 方法以及提供 price 属性。

协议组合

有时候,我们需要一个类型同时遵循多个协议,但这些协议之间没有继承关系。这时可以使用协议组合(Protocol Composition)。协议组合使用 & 符号将多个协议连接起来。

func process(item: Any, requirements: Identifiable & Runner) {
    print("ID: \(requirements.id)")
    requirements.run()
}
let athleteUser = User(id: "456", name: "Jane") as Identifiable & Runner
process(item: athleteUser, requirements: athleteUser)

在上述代码中,process 函数接受一个 Any 类型的 item 和一个同时遵循 IdentifiableRunner 协议的 requirements。通过协议组合,我们可以对满足多个协议要求的类型进行统一处理。

协议的关联类型(Associated Types)

关联类型的概念与定义

关联类型为协议中的某个类型提供了一个占位符名称(或者说别名),具体的类型在遵循协议时才确定。关联类型使用 associatedtype 关键字定义。

例如,定义一个 Container 协议,它有一个关联类型 Item,表示容器中存储的元素类型:

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Container 协议中,append 方法接受一个 Item 类型的参数,subscript 返回一个 Item 类型的值。这里 Item 就是一个关联类型,具体的类型由遵循协议的类型来确定。

遵循包含关联类型的协议

当一个类型遵循包含关联类型的协议时,需要指定关联类型的具体类型。例如,定义一个 Stack 结构体遵循 Container 协议:

struct Stack<Element>: Container {
    // Stack 内部使用数组存储元素
    private var items: [Element] = []
    mutating func append(_ item: Element) {
        items.append(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

这里 Stack 结构体使用泛型 Element 来指定 Container 协议中 Item 关联类型的具体类型。通过这种方式,Stack 结构体满足了 Container 协议的要求。

Swift 扩展(Extensions)基础

扩展的定义与基本使用

扩展是向一个已有的类、结构体、枚举或协议添加新功能的方式。扩展使用 extension 关键字定义。

例如,为 Int 类型添加一个扩展,添加一个计算属性 isEven 来判断一个整数是否为偶数:

extension Int {
    var isEven: Bool {
        return self % 2 == 0
    }
}
let number = 4
print(number.isEven) // true

在上述代码中,通过扩展为 Int 类型添加了 isEven 属性,所有 Int 类型的实例都可以使用这个新属性。

扩展中的方法

扩展不仅可以添加属性,还能添加方法。例如,为 String 类型添加一个方法 reversedString 来返回字符串的反转版本:

extension String {
    func reversedString() -> String {
        return String(self.reversed())
    }
}
let original = "Hello"
let reversed = original.reversedString()
print(reversed) // "olleH"

这里为 String 类型添加了 reversedString 方法,所有 String 类型的实例都可以调用这个方法。

扩展构造器

扩展可以为类、结构体和枚举添加构造器。但是,对于值类型(结构体和枚举),扩展添加的构造器不能覆盖已有的默认构造器或逐一成员构造器。

例如,为 Date 结构体添加一个构造器扩展,用于创建特定格式的日期:

extension Date {
    init(year: Int, month: Int, day: Int) {
        var components = DateComponents()
        components.year = year
        components.month = month
        components.day = day
        let calendar = Calendar.current
        if let date = calendar.date(from: components) {
            self = date
        } else {
            self = Date()
        }
    }
}
let customDate = Date(year: 2023, month: 10, day: 5)
print(customDate)

扩展协议

为协议添加默认实现

通过扩展,可以为协议中的方法或属性提供默认实现。这样,遵循协议的类型就不必强制实现这些有默认实现的部分。

例如,继续上面的 Container 协议,为 isEmpty 属性添加默认实现:

extension Container {
    var isEmpty: Bool {
        return count == 0
    }
}

现在,任何遵循 Container 协议的类型,如 Stack 结构体,都自动拥有了 isEmpty 属性,除非它们自己提供了更具体的实现。

let stack = Stack<Int>()
print(stack.isEmpty) // true

协议扩展中的关联类型

在协议扩展中,也可以使用关联类型。例如,为 Container 协议扩展一个方法 firstItem,返回容器中的第一个元素:

extension Container {
    func firstItem() -> Item? {
        return count > 0? self[0] : nil
    }
}
let stack2 = Stack<Int>()
stack2.append(10)
if let first = stack2.firstItem() {
    print(first) // 10
}

这里在协议扩展中使用了 Item 关联类型,使得协议的功能更加丰富,同时遵循协议的类型也能受益于这些扩展方法。

协议与扩展的高级应用

利用协议和扩展实现多态行为

协议和扩展结合可以实现类似于多态的行为。例如,定义一个 Shape 协议,包含 draw 方法:

protocol Shape {
    func draw()
}

然后为不同的形状类型(如 CircleRectangle)定义结构体并遵循 Shape 协议:

struct Circle: Shape {
    func draw() {
        print("Drawing a circle.")
    }
}
struct Rectangle: Shape {
    func draw() {
        print("Drawing a rectangle.")
    }
}

现在,可以通过扩展为 Shape 协议添加一些通用的行为。例如,添加一个 describe 方法:

extension Shape {
    func describe() {
        print("This is a shape.")
        draw()
    }
}
let circle = Circle()
circle.describe()
// 输出: This is a shape.
//       Drawing a circle.
let rectangle = Rectangle()
rectangle.describe()
// 输出: This is a shape.
//       Drawing a rectangle.

通过这种方式,不同的形状类型虽然有各自的 draw 实现,但都能通过 describe 方法表现出多态的行为。

协议扩展与类型约束

在协议扩展中,可以使用类型约束来限制扩展的适用范围。例如,定义一个 NumericContainer 协议,它继承自 Container 协议,并要求 Item 关联类型必须是 Numeric 类型:

protocol NumericContainer: Container where Item: Numeric {
    func sum() -> Item
}

然后为 NumericContainer 协议提供扩展实现 sum 方法:

extension NumericContainer {
    func sum() -> Item {
        var result: Item = .zero
        for i in 0..<count {
            result += self[i]
        }
        return result
    }
}
struct IntStack: NumericContainer {
    private var items: [Int] = []
    mutating func append(_ item: Int) {
        items.append(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Int {
        return items[i]
    }
}
let intStack = IntStack()
intStack.append(1)
intStack.append(2)
intStack.append(3)
let sum = intStack.sum()
print(sum) // 6

这里通过类型约束 where Item: Numeric,确保了 sum 方法只适用于 ItemNumeric 类型的 NumericContainer 遵循者。

利用协议和扩展进行代码复用与解耦

协议和扩展可以帮助我们实现代码复用和解耦。例如,在一个游戏开发中,可能有不同类型的角色,如 WarriorMage 等,它们都需要一些通用的行为,如 move

protocol Movable {
    func move()
}
extension Movable {
    func move() {
        print("Moving...")
    }
}
class Warrior: Movable {}
class Mage: Movable {}
let warrior = Warrior()
warrior.move()
let mage = Mage()
mage.move()

通过将 move 行为定义在协议扩展中,不同的角色类只需要遵循 Movable 协议,就可以复用这些代码,同时各个角色类之间也实现了解耦,它们不需要知道其他角色类的具体实现,只关注自己遵循的协议即可。

协议和扩展在实际项目中的应用场景

数据层抽象

在开发应用程序时,数据层通常需要从不同的数据源(如本地数据库、网络 API 等)获取数据。可以定义一个协议来抽象数据获取的操作。

protocol DataFetcher {
    associatedtype DataType
    func fetchData() -> DataType?
}

然后为不同的数据源实现这个协议。例如,为本地数据库实现 LocalDataFetcher

class LocalDataFetcher: DataFetcher {
    typealias DataType = [String: Any]
    func fetchData() -> [String: Any]? {
        // 从本地数据库获取数据的逻辑
        return ["key": "value"]
    }
}

为网络 API 实现 NetworkDataFetcher

class NetworkDataFetcher: DataFetcher {
    typealias DataType = Data
    func fetchData() -> Data? {
        // 从网络获取数据的逻辑
        return nil
    }
}

通过协议和扩展,可以为 DataFetcher 协议添加通用的功能,如数据缓存逻辑:

extension DataFetcher {
    private var cache: [DataType] = []
    func fetchDataWithCache() -> DataType? {
        if let data = cache.first {
            return data
        }
        if let data = fetchData() {
            cache.append(data)
            return data
        }
        return nil
    }
}

这样,在业务层只需要关注 DataFetcher 协议,而不需要关心具体的数据源实现,实现了数据层的抽象和解耦。

视图层复用

在 iOS 开发中,视图层的复用非常重要。例如,可以定义一个 ReusableView 协议,用于标识可以复用的视图:

protocol ReusableView {
    static var reuseIdentifier: String { get }
}
extension ReusableView {
    static var reuseIdentifier: String {
        return String(describing: self)
    }
}

然后,让 UITableViewCell 的子类遵循这个协议:

import UIKit
class CustomTableViewCell: UITableViewCell, ReusableView {
    // 单元格的具体实现
}

UITableView 的数据源方法中,可以方便地使用这个协议来复用单元格:

class ViewController: UIViewController, UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.reuseIdentifier, for: indexPath) as! CustomTableViewCell
        // 配置单元格
        return cell
    }
    // 其他数据源方法
}

通过协议和扩展,使得视图层的复用更加规范化和便捷。

依赖注入与测试

在进行单元测试时,协议和扩展可以帮助实现依赖注入,提高代码的可测试性。例如,假设一个 UserService 类依赖于一个 NetworkManager 来获取用户数据:

protocol NetworkManagerProtocol {
    func fetchUser() -> User?
}
class NetworkManager: NetworkManagerProtocol {
    func fetchUser() -> User? {
        // 实际的网络请求逻辑
        return nil
    }
}
class UserService {
    let networkManager: NetworkManagerProtocol
    init(networkManager: NetworkManagerProtocol) {
        self.networkManager = networkManager
    }
    func getUser() -> User? {
        return networkManager.fetchUser()
    }
}

在测试 UserService 时,可以创建一个模拟的 NetworkManager 来注入到 UserService 中:

class MockNetworkManager: NetworkManagerProtocol {
    func fetchUser() -> User? {
        return User(id: "1", name: "Mock User")
    }
}
class UserServiceTests: XCTestCase {
    func testUserService() {
        let mockNetworkManager = MockNetworkManager()
        let userService = UserService(networkManager: mockNetworkManager)
        let user = userService.getUser()
        XCTAssertNotNil(user)
    }
}

通过协议和扩展,可以方便地为不同的测试场景提供不同的依赖实现,提高了代码的可测试性和灵活性。

协议与扩展的注意事项

协议一致性检查

在类型遵循协议时,编译器会严格检查是否满足协议的所有要求。如果遗漏了某个要求的实现,编译器会报错。例如,一个类遵循 Identifiable 协议但没有实现 id 属性:

class MissingImplementation: Identifiable {
    // 错误:Type 'MissingImplementation' does not conform to protocol 'Identifiable'
    // 因为没有实现 'id' 属性
}

所以在遵循协议时,务必确保实现了协议中的所有属性、方法和初始化器要求。

扩展的命名冲突

在为已有类型添加扩展时,要注意避免命名冲突。如果扩展中定义的属性或方法与类型原有的属性或方法同名,可能会导致难以调试的问题。例如,为 String 类型扩展一个与原方法同名的方法:

extension String {
    func hasPrefix(_ prefix: String) -> Bool {
        // 自定义实现
        return false
    }
}
let str = "Hello"
let result = str.hasPrefix("He")
// 这里调用的是扩展中的方法,而不是 String 原有的方法,可能不符合预期

因此,在扩展时要谨慎选择命名,尽量避免与已有成员冲突。

协议扩展的局限性

虽然协议扩展可以为协议提供默认实现,但它也有一些局限性。例如,协议扩展不能为协议添加存储属性,只能添加计算属性。而且,协议扩展中的默认实现不能访问协议中定义的存储属性,除非这些属性是通过 get 方法暴露出来的。例如:

protocol Storable {
    var data: String { get }
}
extension Storable {
    func printDataLength() {
        // 这里可以访问 data 属性,因为它有 get 方法
        print("Data length: \(data.count)")
    }
}

如果协议中的属性没有 get 方法,在协议扩展中就无法访问。

总之,在使用 Swift 的协议与扩展时,要充分理解它们的特性、应用场景以及注意事项,这样才能编写出清晰、可维护且高效的代码。无论是在小型项目还是大型项目中,协议与扩展都能成为强大的工具,帮助我们实现代码的复用、解耦和抽象。通过合理运用它们,我们可以提高代码的质量和开发效率,构建出更加健壮和灵活的应用程序。