如何将类型擦除与使用关联类型的协议一起使用
How can I use Type Erasure with a protocol using associated type
我正在从事一个项目,该项目的网络客户端基本上遵循以下模式。
protocol EndpointType {
var baseURL: String { get }
}
enum ProfilesAPI {
case fetchProfileForUser(id: String)
}
extension ProfilesAPI: EndpointType {
var baseURL: String {
return "https://foo.bar"
}
}
protocol ClientType: class {
associatedtype T: EndpointType
func request(_ request: T) -> Void
}
class Client<T: EndpointType>: ClientType {
func request(_ request: T) -> Void {
print(request.baseURL)
}
}
let client = Client<ProfilesAPI>()
client.request(.fetchProfileForUser(id: "123"))
作为整理此项目和编写测试的一部分,我发现在遵守 ClientType
协议时无法注入 client
。
let client: ClientType = Client<ProfilesAPI>()
产生错误:
error: member 'request' cannot be used on value of protocol type
'ClientType'; use a generic constraint instead
我想保持目前的格局... = Client<ProfilesAPI>()
是否可以使用类型擦除来实现?我一直在阅读,但不确定如何进行这项工作。
对于您的实际问题,类型橡皮擦很简单:
final class AnyClient<T: EndpointType>: ClientType {
let _request: (T) -> Void
func request(_ request: T) { _request(request) }
init<Client: ClientType>(_ client: Client) where Client.T == T {
_request = client.request
}
}
对于协议中的每项要求,您都需要这些 _func/func
对之一。你可以这样使用它:
let client = AnyClient(Client<ProfilesAPI>())
然后您可以创建一个测试工具,例如:
class RecordingClient<T: EndpointType>: ClientType {
var requests: [T] = []
func request(_ request: T) -> Void {
requests.append(request)
print("recording: \(request.baseURL)")
}
}
改用那个:
let client = AnyClient(RecordingClient<ProfilesAPI>())
但如果可以避免的话,我真的不推荐这种方法。类型橡皮擦令人头疼。相反,我会查看 Client
的内部,并将非通用部分提取到不需要 T
的 ClientEngine
协议中。然后在构造 Client
时使 that 可交换。这样你就不需要类型擦除器,也不必向调用者公开额外的协议(只是 EndpointType)。
例如引擎部分:
protocol ClientEngine: class {
func request(_ request: String) -> Void
}
class StandardClientEngine: ClientEngine {
func request(_ request: String) -> Void {
print(request)
}
}
持有引擎的客户端。请注意它如何使用默认参数,以便调用者无需更改任何内容。
class Client<T: EndpointType> {
let engine: ClientEngine
init(engine: ClientEngine = StandardClientEngine()) { self.engine = engine }
func request(_ request: T) -> Void {
engine.request(request.baseURL)
}
}
let client = Client<ProfilesAPI>()
再一次,录音版本:
class RecordingClientEngine: ClientEngine {
var requests: [String] = []
func request(_ request: String) -> Void {
requests.append(request)
print("recording: \(request)")
}
}
let client = Client<ProfilesAPI>(engine: RecordingClientEngine())
我正在从事一个项目,该项目的网络客户端基本上遵循以下模式。
protocol EndpointType {
var baseURL: String { get }
}
enum ProfilesAPI {
case fetchProfileForUser(id: String)
}
extension ProfilesAPI: EndpointType {
var baseURL: String {
return "https://foo.bar"
}
}
protocol ClientType: class {
associatedtype T: EndpointType
func request(_ request: T) -> Void
}
class Client<T: EndpointType>: ClientType {
func request(_ request: T) -> Void {
print(request.baseURL)
}
}
let client = Client<ProfilesAPI>()
client.request(.fetchProfileForUser(id: "123"))
作为整理此项目和编写测试的一部分,我发现在遵守 ClientType
协议时无法注入 client
。
let client: ClientType = Client<ProfilesAPI>()
产生错误:
error: member 'request' cannot be used on value of protocol type 'ClientType'; use a generic constraint instead
我想保持目前的格局... = Client<ProfilesAPI>()
是否可以使用类型擦除来实现?我一直在阅读,但不确定如何进行这项工作。
对于您的实际问题,类型橡皮擦很简单:
final class AnyClient<T: EndpointType>: ClientType {
let _request: (T) -> Void
func request(_ request: T) { _request(request) }
init<Client: ClientType>(_ client: Client) where Client.T == T {
_request = client.request
}
}
对于协议中的每项要求,您都需要这些 _func/func
对之一。你可以这样使用它:
let client = AnyClient(Client<ProfilesAPI>())
然后您可以创建一个测试工具,例如:
class RecordingClient<T: EndpointType>: ClientType {
var requests: [T] = []
func request(_ request: T) -> Void {
requests.append(request)
print("recording: \(request.baseURL)")
}
}
改用那个:
let client = AnyClient(RecordingClient<ProfilesAPI>())
但如果可以避免的话,我真的不推荐这种方法。类型橡皮擦令人头疼。相反,我会查看 Client
的内部,并将非通用部分提取到不需要 T
的 ClientEngine
协议中。然后在构造 Client
时使 that 可交换。这样你就不需要类型擦除器,也不必向调用者公开额外的协议(只是 EndpointType)。
例如引擎部分:
protocol ClientEngine: class {
func request(_ request: String) -> Void
}
class StandardClientEngine: ClientEngine {
func request(_ request: String) -> Void {
print(request)
}
}
持有引擎的客户端。请注意它如何使用默认参数,以便调用者无需更改任何内容。
class Client<T: EndpointType> {
let engine: ClientEngine
init(engine: ClientEngine = StandardClientEngine()) { self.engine = engine }
func request(_ request: T) -> Void {
engine.request(request.baseURL)
}
}
let client = Client<ProfilesAPI>()
再一次,录音版本:
class RecordingClientEngine: ClientEngine {
var requests: [String] = []
func request(_ request: String) -> Void {
requests.append(request)
print("recording: \(request)")
}
}
let client = Client<ProfilesAPI>(engine: RecordingClientEngine())