首页 Swift 中 Protocol 和 泛型
文章
取消

Swift 中 Protocol 和 泛型

前言

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

1
2
3
4
5
6
7
8
/// 类
class AClass<T> {}

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

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

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

1
protocol AProtocol<T> {}

报错信息:

Protocols do not allow generic parameters; use associated types instead

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

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

正文

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

1
2
3
4
5
6
7
8
9
10
11
12
/// 请求老师数据列表
/// - 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 地址.

1
2
3
4
5
6
7
8
9
10
11
protocol PListable {

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension PListable {
    
    /// 分页的方式请求数据列表
    /// - Parameter parameters: 参数对象
    /// - Returns: 请求结果
    static func plist(parameters: Parameters) -> Result? {
        /*
         网络请求代码
         ...
         */
        /// 网络请求取到的数据
        let data: Data = ...
        /// 解析数据
        return try? JSONDecoder().decode(Result.self, from: data)
    }
}

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

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

参数类型数据结构

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

1
2
3
4
5
6
7
8
struct PLimit: Encodable {
    
    /// 分页页码
    let page: Int
    
    /// 分页数据容量
    let limit: Int
}

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

1
2
3
4
5
6
7
8
9
10
11
struct PLimitWithId: Encodable {
    
    /// 数据查询依赖的 id
    let id: Int
    
    /// 分页页码
    let page: Int
    
    /// 分页数据容量
    let limit: Int
}

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

数据体数据结构

1
2
3
4
5
6
7
8
9
/// 老师对象
struct Teacher: Decodable {
    
    /// 姓名
    var name: String?
    
    /// 教学科目列表
    var subject: [Subject]?
}

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

1
2
3
4
5
6
7
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 做相同的操作.

1
2
3
4
5
6
/// 科目对象
struct Subject: Decodable {
    
    /// 科目名称
    var name: String?
}

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

1
2
3
4
5
6
7
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 类型的数据.

调用的代码如下:

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

扩展

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

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

1
2
3
4
5
6
7
8
9
10
11
protocol Animal {

    associatedtype `Type`
}

struct Cat<T> {}

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

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

结语

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

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

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

本文由作者按照 CC BY 4.0 进行授权