Swift 中 Protocol 和 泛型

04/28/2023 18:05 下午 posted in  apple

前言

一般在 Swift 中使用 泛型 的时候我们会这么写:

/// 类
class AClass<T> {}

/// 结构体
struct ASctuct<T> {}

/// 枚举
enum AEnum<T> {}

但是如果想在 协议 中使用泛型的时候这么写就会报错:

protocol AProtocol<T> {}

报错信息:

Protocols do not allow generic parameters; use associated types instead

虽然 泛型 可以在 类, 结构体, 枚举 中使用, 但是某些使用场景中, 如果在 协议 中加入 泛型 的话, 会使我们的代码更加灵活.

尽管 协议 中不支持 泛型, 但是却有个 associatedtype, 各种文章和书籍中都把它翻译为 关联类型. 我们可以使用 associatedtype 来达成 泛型 的目的.

正文
假设现在有如下 2 个接口:

/// 请求老师数据列表
/// - page:  分页页码
/// - limit: 分页页面容量
/// - return: 老师列表数据
[POST] https://example.com/teachlist

/// 请求老师所教授的科目
/// - id:     老师 id
/// - page:   分页页码
/// - limit:  分页页面容量
/// - return: 老师教授的科目数据列表
[POST] https://example.com/subjectlist

PListable 协议

此处定义协议 PListable.

Parameters 为网络请求的参数类型, 由于其需要使用 JSONEncoder 对其进行编码, 因此需要实现 Encodable 协议.

Result 作为请求方法的返回类型, 由于需要使用 JSONDecoder 对请求到的 Data 进行解码, 因此需要实现 Decodable 协议.

requestURL 返回结果为网络请求的 URL 地址.

protocol PListable {

    /// 参数类型
    associatedtype Parameters: Encodable
    
    /// 请求结果类型
    associatedtype Result: Decodable
    
    /// 请求地址
    static var requestURL: URL? { get }
}

在协议的 extension 中实现了 static func plist(parameters: Parameters) -> Result? , 该方法为实现该协议的类型提供网络请求的功能实现.

extension PListable {
    
    /// 分页的方式请求数据列表
    /// - Parameter parameters: 参数对象
    /// - Returns: 请求结果
    static func plist(parameters: Parameters) -> Result? {
        /*
         网络请求代码
         ...
         */
        /// 网络请求取到的数据
        let data: Data = ...
        /// 解析数据
        return try? JSONDecoder().decode(Result.self, from: data)
    }
}

此方法为了更加清晰的表达意图, 未使用 异步, 而是使用了 同步 的直接返回请求结果的写法.

如果了解 协程 的话, 应该就很容易理解这种写法了.

参数类型数据结构

PLimit 结构为需要 page 和 limit 参数类型的接口提供参数. 依据 PListable 协议中 Parameters 的约束要求实现了 Encodable 协议.

struct PLimit: Encodable {
    
    /// 分页页码
    let page: Int
    
    /// 分页数据容量
    let limit: Int
}

PLimitWithId 结构对应的为需要 id, page, limit 参数类型的接口提供参数, 同样的实现了 Encodable 协议.

struct PLimitWithId: Encodable {
    
    /// 数据查询依赖的 id
    let id: Int
    
    /// 分页页码
    let page: Int
    
    /// 分页数据容量
    let limit: Int
}

Teacher 为接口 https://example.com/teachlist 返回的数据体部分的数据结构. 根据 PListable 协议中 Result 类型约束的要求实现了 Decodable 协议.

数据体数据结构

/// 老师对象
struct Teacher: Decodable {
    
    /// 姓名
    var name: String?
    
    /// 教学科目列表
    var subject: [Subject]?
}

Teacher 实现 PListable 协议, 并在 extension 中给 Parameters 类型关联为 PLimit, Result 类型关联为 [Teacher] 类型.

extension Teacher: PListable {

    typealias Parameters = PLimit
    typealias Result = [Teacher]

    static var requestURL: URL? { URL(string: "http://example.com/teachlist") }
}

这样 Teacher 就可以调用 static func plist(parameters: Parameters) -> Result? 方法了, 并且其参数类型为 PLimit, 返回类型为 [Teacher] 返回一组 Teacher 类型的数据.

对应的, Subject 也与 Teacher 做相同的操作.

/// 科目对象
struct Subject: Decodable {
    
    /// 科目名称
    var name: String?
}

不同的是 Subject 中 Parameters 绑定为 PLimitWithId 类型, Result 绑定为 [Subject] 类型.

extension Subject: PListable {
    
    typealias Parameters = PLimitWithId
    typealias Result = [Subject]

    static var requestURL: URL? { URL(string: "http://example.com/subjectlist") }
}

这样 Subject 就同样可以调用 static func plist(parameters: Parameters) -> Result? 方法了, 并且其参数类型为 PLimitWithId, 返回类型为 [Subject] 返回一组 Subject 类型的数据.

调用的代码如下:

Teacher.plist(parameters: PLimit(page: 0, limit: 20))
Subject.plist(parameters: PLimitWithId(id: 101, page: 0, limit: 20))

扩展

同时 protocol + associatedtype 还可以与 泛型 组合使用:

如果我们有如下 Animal 协议 和 结构体 Cat:

protocol Animal {

    associatedtype `Type`
}

struct Cat<T> {}

extension Cat: Animal {
    
    typealias `Type` = T
}

Cat 类型接收一个 T 类型的泛型, Cat 在实现 Animal 协议后, 可以把 T 设置为 Type 的关联类型.

结语

虽然使用 class 的 继承 也能达到类似的效果, 但是 struct 和 enum 却不支持 继承.

通过 协议 任何实现 PListable 的类型都拥有了 分页获取数据 的能力.

在项目开发中我们往往可能还要有 Deleteable, Updateable … 等等诸多类型的接口, 如果我们都通过 protocol + associatedtype 的方式来为对应类型进行扩展, 不仅能够提升开发效率, 还能降低维护成本.