前言
一般在 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
结构为需要 page
和 limit
参数类型的接口提供参数. 依据 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?
}
不同的是 Subject
中 Parameters
绑定为 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
的 继承
也能达到类似的效果, 但是 struct
和 enum
却不支持 继承
.
通过 协议
任何实现 PListable
的类型都拥有了 分页获取数据
的能力.
在项目开发中我们往往可能还要有 Deleteable
, Updateable
… 等等诸多类型的接口, 如果我们都通过 protocol
+ associatedtype
的方式来为对应类型进行扩展, 不仅能够提升开发效率, 还能降低维护成本.