Swift疑问:继承Encodable的Protocol为何不遵循该协议?
最近有朋友碰到这么个坑:明明定义的协议已经继承了Encodable,但遵循协议的结构体却没法自动合成编码实现,报错说协议不遵循Encodable。咱们一起来看看怎么回事。
问题还原
先看一下他写的代码:
首先定义了两个继承自Encodable的协议:
protocol Filters: Encodable { var page: Int { get } } protocol Parameters: Encodable { var type: String { get } var filters: Filters { get } }
然后创建了对应的结构体实现:
struct BankAccountFilters: Filters { var page: Int var isWithdrawal: Bool } struct BankAccountParameters: Parameters { let type: String = "Bank" var filters: Filters } let baf = BankAccountFilters(page: 1, isWithdrawal: true) let bap = BankAccountParameters(filters: baf)
结果编译直接报错:
error: type 'BankAccountParameters' does not conform to protocol 'Encodable'
note: cannot automatically synthesize 'Encodable' because 'Filters' does not conform to 'Encodable'
明明Filters已经继承了Encodable,这报错看起来就很矛盾对吧?
为啥会报错?
其实问题出在Swift自动合成Encodable的规则上:Swift要自动生成编码逻辑,必须知道所有存储属性的具体类型都遵循Encodable。虽然Filters协议本身继承了Encodable,但协议类型本身并不是一个具体的可编码类型——Swift不知道你最终会把哪个遵循Filters的结构体赋值给filters属性,它没法提前确定编码的具体逻辑,自然就没法自动合成了。
解决办法
这里有几种实用的解决方案,你可以根据自己的业务场景选:
方案一:把Parameters改成泛型协议
让Parameters协议关联一个具体的Filters类型,这样Swift就能明确知道filters的具体类型,从而自动合成Encodable:
protocol Filters: Encodable { var page: Int { get } } protocol Parameters: Encodable { associatedtype FilterType: Filters var type: String { get } var filters: FilterType { get } } struct BankAccountFilters: Filters { var page: Int var isWithdrawal: Bool } struct BankAccountParameters: Parameters { let type: String = "Bank" var filters: BankAccountFilters // 这里用具体的结构体类型 } // 现在可以正常编码了 let baf = BankAccountFilters(page: 1, isWithdrawal: true) let bap = BankAccountParameters(filters: baf) try JSONEncoder().encode(bap)
方案二:手动实现Encodable方法
如果不想用泛型,也可以直接给BankAccountParameters手动写encode(to:)方法,明确告诉Swift怎么编码filters属性:
protocol Filters: Encodable { var page: Int { get } } protocol Parameters: Encodable { var type: String { get } var filters: Filters { get } } struct BankAccountFilters: Filters { var page: Int var isWithdrawal: Bool } struct BankAccountParameters: Parameters { let type: String = "Bank" var filters: Filters // 手动实现编码逻辑 func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type, forKey: .type) // 直接编码filters,因为它本身遵循Encodable try container.encode(filters, forKey: .filters) } // 定义编码的Key enum CodingKeys: String, CodingKey { case type, filters } } let baf = BankAccountFilters(page: 1, isWithdrawal: true) let bap = BankAccountParameters(filters: baf) try JSONEncoder().encode(bap)
方案三:用类型擦除包装协议类型
如果你的业务需要支持多种Filters类型,还可以用类型擦除把协议类型包装成一个具体的结构体AnyFilters,这样就能满足自动合成的要求了:
protocol Filters: Encodable { var page: Int { get } } // 类型擦除的结构体,把Filters协议包装成具体类型 struct AnyFilters: Filters, Encodable { let page: Int private let encodeClosure: (Encoder) throws -> Void init<F: Filters>(_ filters: F) { self.page = filters.page self.encodeClosure = { encoder in try filters.encode(to: encoder) } } func encode(to encoder: Encoder) throws { try encodeClosure(encoder) } } protocol Parameters: Encodable { var type: String { get } var filters: AnyFilters { get } } struct BankAccountFilters: Filters { var page: Int var isWithdrawal: Bool } struct BankAccountParameters: Parameters { let type: String = "Bank" var filters: AnyFilters } let baf = BankAccountFilters(page: 1, isWithdrawal: true) let bap = BankAccountParameters(filters: AnyFilters(baf)) try JSONEncoder().encode(bap)
内容的提问来源于stack exchange,提问作者Ashley Mills

