最终 class 的多态性实现了 swift 中的关联类型协议
Polymorphism with a final class that implements an associatedtype protocol in swift
我正在使用 Apollo v0.49.0。它是一个用于调用 graphQL 端点的库,其实现方式是在编译代码之前生成代码。
在说生成代码之前,先说一下生成代码实现了什么。对于这个问题,相关的是 GraphQLMutation
。这是它的样子:
public enum GraphQLOperationType {
case query
case mutation
case subscription
}
public protocol GraphQLOperation: AnyObject {
var operationType: GraphQLOperationType { get }
var operationDefinition: String { get }
var operationIdentifier: String? { get }
var operationName: String { get }
var queryDocument: String { get }
var variables: GraphQLMap? { get }
associatedtype Data: GraphQLSelectionSet
}
public extension GraphQLOperation {
var queryDocument: String {
return operationDefinition
}
var operationIdentifier: String? {
return nil
}
var variables: GraphQLMap? {
return nil
}
}
public protocol GraphQLQuery: GraphQLOperation {}
public extension GraphQLQuery {
var operationType: GraphQLOperationType { return .query }
}
public protocol GraphQLMutation: GraphQLOperation {}
public extension GraphQLMutation {
var operationType: GraphQLOperationType { return .mutation }
}
这是 the file 的 80%;最后 20% 是无关紧要的恕我直言。请注意 GraphQLMutation
如何实现 GraphQLOperation
而后者有一个 associatedtype
.
该库根据您的 graphql 服务器端点生成 classes。它们的外观如下:
public final class ConcreteMutation: GraphQLMutation {
...
public struct Data: GraphQLSelectionSet {
...
}
...
}
据我所知(我是 Swift 的新手),我无法控制我目前提到的任何代码(除了分叉回购和修改它)。我可以在本地更改它们,但每次重新生成时它们都会被覆盖。
要使用这些生成的任何 classes,我必须将它们传递给这个 ApolloClient
函数(也是一个库 class):
@discardableResult
public func perform<Mutation: GraphQLMutation>(mutation: Mutation,
publishResultToStore: Bool = true,
queue: DispatchQueue = .main,
resultHandler: GraphQLResultHandler<Mutation.Data>? = nil) -> Cancellable {
return self.networkTransport.send(
operation: mutation,
cachePolicy: publishResultToStore ? .default : .fetchIgnoringCacheCompletely,
contextIdentifier: nil,
callbackQueue: queue,
completionHandler: { result in
resultHandler?(result)
}
)
}
我不知道如何以通用方式处理 ConcreteMutation
。我希望能够像这样编写一个工厂函数:
extension SomeEnum {
func getMutation<T: GraphQLMutation>() -> T {
switch self {
case .a:
return ConcreteMutation1(first_name: value) as T
case .b:
return ConcreteMutation2(last_name: value) as T
case .c:
return ConcreteMutation3(bio: value) as T
...
}
}
}
这个函数在枚举中的事实与我无关:相同的代码可能在 struct/class/whatever 中。 重要的是函数签名。我想要一个 returns 一个 GraphQLMutation
可以传递给 ApolloClient.perform()
的工厂方法
因为我想不出一种方法来做这两件事,所以我最终写了一堆这样的函数:
func useConcreteMutation1(value) -> Void {
let mutation = ConcreteMutation1(first_name: value)
apolloClient.perform(mutation: mutation)
}
func useConcreteMutation2(value) -> Void {
let mutation = ConcreteMutation2(last_name: value)
apolloClient.perform(mutation: mutation)
}
...
这是很多重复的代码。
取决于我如何 fiddle 我的 getMutation
签名——例如,<T: GraphQLMutation>() -> T?
等——我可以得到要编译的函数,但我得到了不同的编译错误当我尝试将它传递给 ApolloClient.perform()
时。说“协议只能用作通用约束,因为它具有自我或相关类型要求。”
我对此进行了很多研究,我的研究发现 this article,但如果实现关联类型的具体 classes 是最终的,我认为这不是一个选项?
在这种情况下真的很难弄清楚是否可以使用多态性。我可以找到很多关于您可以 做什么的文章,但是找不到关于您不能 做什么的文章。我的问题是:
如何编写 getMutation
以便它 returns 一个可以传递给 ApolloClient.perform()
的值?
也许您需要在 associatedtype
上实现 AnyGraphQLMutation
类型擦除。
关于这件事(类型擦除),网上有很多资源,我发现 this one 非常详尽。
您 运行 遇到的根本问题是这个函数签名:
func getMutation<T: GraphQLMutation>() -> T
是模棱两可的。它不明确的原因是因为 GraphQLMutation 具有关联类型 (Data
),并且该信息不会出现在函数声明中的任何位置。
当你这样做时:
extension SomeEnum {
func getMutation<T: GraphQLMutation>() -> T {
switch self {
case .a:
return ConcreteMutation1(first_name: value) as T
case .b:
return ConcreteMutation2(last_name: value) as T
case .c:
return ConcreteMutation3(bio: value) as T
...
}
}
}
这些分支中的每一个都可以有不同的类型。 ConcreteMutation1
可能有一个 Data
即 Dormouse
而 ConcreteMutation3
可能有一个 IceCreamTruck
的数据值。您也许可以告诉编译器忽略它,但是稍后您 运行 会遇到问题,因为 Dormouse
和 IceCreamTruck
是两个大小非常不同的结构,编译器可能需要使用不同的策略将它们作为参数传递。
Apollo.perform
也是一个模板。编译器将基于该模板为您调用它的每种类型的突变编写不同的函数。为了做到这一点 必须 知道突变的完整类型签名,包括它的 Data
关联类型是什么。 responseHandler
回调应该能够处理 Dormouse
大小的东西,还是需要能够处理 IceCreamTruck
大小的东西?
如果编译器不知道,它就无法为 responseHandler
设置正确的调用顺序。如果您试图通过为 Dormouse
!
大小的参数设计的回调调用序列来压缩 IceCreamTruck
大小的内容,就会发生不好的事情
如果编译器不知道变异必须提供什么类型的 Data
,它就不能从模板中写出 perform
的正确版本。
如果您只将 func getMutation<T: GraphQLMutation>() -> T
的结果交给它,您已经消除了 Data
类型的证据,它不知道 perform
的版本它应该写。
您试图隐藏 Data
的类型,但您还希望编译器创建一个 perform
函数,其中 Data
的类型是已知的。你不能两者都做。
希望这对您有所帮助:
class GraphQLQueryHelper
{
static let shared = GraphQLQueryHelper()
class func performGraphQLQuery<T:GraphQLQuery>(query: T, completion:@escaping(GraphQLSelectionSet) -> ())
{
Network.shared.apollo().fetch(query: query, cachePolicy: .default) { (result) in
switch result
{
case .success(let res):
if let data = res.data
{
completion(data)
}
else if let error = res.errors?.first
{
if let dict = error["extensions"] as? NSDictionary
{
switch dict.value(forKey: "code") as? String ?? "" {
case "invalid-jwt": /*Handle Refresh Token Expired*/
default: /*Handle error*/
break
}
}
else
{
/*Handle error*/
}
}
else
{
/*Handle Network error*/
}
break
case .failure(let error):
/*Handle Network error*/
break
}
}
}
class func peroformGraphQLMutation<T:GraphQLMutation>(mutation: T, completion:@escaping(GraphQLSelectionSet) -> ())
{
Network.shared.apollo().perform(mutation: mutation) { (result) in
switch result
{
case .success(let res):
if let data = res.data
{
completion(data)
}
else if let error = res.errors?.first
{
if let dict = error["extensions"] as? NSDictionary
{
switch dict.value(forKey: "code") as? String ?? "" {
case "invalid-jwt": /*Handle Refresh Token Expired*/
default: /*Handle error*/
break
}
}
else
{
/*Handle error*/
}
}
else
{
/*Handle Network error*/
}
break
case .failure(let error):
/*Handle error*/
break
}
}
}
}
我正在使用 Apollo v0.49.0。它是一个用于调用 graphQL 端点的库,其实现方式是在编译代码之前生成代码。
在说生成代码之前,先说一下生成代码实现了什么。对于这个问题,相关的是 GraphQLMutation
。这是它的样子:
public enum GraphQLOperationType {
case query
case mutation
case subscription
}
public protocol GraphQLOperation: AnyObject {
var operationType: GraphQLOperationType { get }
var operationDefinition: String { get }
var operationIdentifier: String? { get }
var operationName: String { get }
var queryDocument: String { get }
var variables: GraphQLMap? { get }
associatedtype Data: GraphQLSelectionSet
}
public extension GraphQLOperation {
var queryDocument: String {
return operationDefinition
}
var operationIdentifier: String? {
return nil
}
var variables: GraphQLMap? {
return nil
}
}
public protocol GraphQLQuery: GraphQLOperation {}
public extension GraphQLQuery {
var operationType: GraphQLOperationType { return .query }
}
public protocol GraphQLMutation: GraphQLOperation {}
public extension GraphQLMutation {
var operationType: GraphQLOperationType { return .mutation }
}
这是 the file 的 80%;最后 20% 是无关紧要的恕我直言。请注意 GraphQLMutation
如何实现 GraphQLOperation
而后者有一个 associatedtype
.
该库根据您的 graphql 服务器端点生成 classes。它们的外观如下:
public final class ConcreteMutation: GraphQLMutation {
...
public struct Data: GraphQLSelectionSet {
...
}
...
}
据我所知(我是 Swift 的新手),我无法控制我目前提到的任何代码(除了分叉回购和修改它)。我可以在本地更改它们,但每次重新生成时它们都会被覆盖。
要使用这些生成的任何 classes,我必须将它们传递给这个 ApolloClient
函数(也是一个库 class):
@discardableResult
public func perform<Mutation: GraphQLMutation>(mutation: Mutation,
publishResultToStore: Bool = true,
queue: DispatchQueue = .main,
resultHandler: GraphQLResultHandler<Mutation.Data>? = nil) -> Cancellable {
return self.networkTransport.send(
operation: mutation,
cachePolicy: publishResultToStore ? .default : .fetchIgnoringCacheCompletely,
contextIdentifier: nil,
callbackQueue: queue,
completionHandler: { result in
resultHandler?(result)
}
)
}
我不知道如何以通用方式处理 ConcreteMutation
。我希望能够像这样编写一个工厂函数:
extension SomeEnum {
func getMutation<T: GraphQLMutation>() -> T {
switch self {
case .a:
return ConcreteMutation1(first_name: value) as T
case .b:
return ConcreteMutation2(last_name: value) as T
case .c:
return ConcreteMutation3(bio: value) as T
...
}
}
}
这个函数在枚举中的事实与我无关:相同的代码可能在 struct/class/whatever 中。 重要的是函数签名。我想要一个 returns 一个 GraphQLMutation
可以传递给 ApolloClient.perform()
因为我想不出一种方法来做这两件事,所以我最终写了一堆这样的函数:
func useConcreteMutation1(value) -> Void {
let mutation = ConcreteMutation1(first_name: value)
apolloClient.perform(mutation: mutation)
}
func useConcreteMutation2(value) -> Void {
let mutation = ConcreteMutation2(last_name: value)
apolloClient.perform(mutation: mutation)
}
...
这是很多重复的代码。
取决于我如何 fiddle 我的 getMutation
签名——例如,<T: GraphQLMutation>() -> T?
等——我可以得到要编译的函数,但我得到了不同的编译错误当我尝试将它传递给 ApolloClient.perform()
时。说“协议只能用作通用约束,因为它具有自我或相关类型要求。”
我对此进行了很多研究,我的研究发现 this article,但如果实现关联类型的具体 classes 是最终的,我认为这不是一个选项?
在这种情况下真的很难弄清楚是否可以使用多态性。我可以找到很多关于您可以 做什么的文章,但是找不到关于您不能 做什么的文章。我的问题是:
如何编写 getMutation
以便它 returns 一个可以传递给 ApolloClient.perform()
的值?
也许您需要在 associatedtype
上实现 AnyGraphQLMutation
类型擦除。
关于这件事(类型擦除),网上有很多资源,我发现 this one 非常详尽。
您 运行 遇到的根本问题是这个函数签名:
func getMutation<T: GraphQLMutation>() -> T
是模棱两可的。它不明确的原因是因为 GraphQLMutation 具有关联类型 (Data
),并且该信息不会出现在函数声明中的任何位置。
当你这样做时:
extension SomeEnum {
func getMutation<T: GraphQLMutation>() -> T {
switch self {
case .a:
return ConcreteMutation1(first_name: value) as T
case .b:
return ConcreteMutation2(last_name: value) as T
case .c:
return ConcreteMutation3(bio: value) as T
...
}
}
}
这些分支中的每一个都可以有不同的类型。 ConcreteMutation1
可能有一个 Data
即 Dormouse
而 ConcreteMutation3
可能有一个 IceCreamTruck
的数据值。您也许可以告诉编译器忽略它,但是稍后您 运行 会遇到问题,因为 Dormouse
和 IceCreamTruck
是两个大小非常不同的结构,编译器可能需要使用不同的策略将它们作为参数传递。
Apollo.perform
也是一个模板。编译器将基于该模板为您调用它的每种类型的突变编写不同的函数。为了做到这一点 必须 知道突变的完整类型签名,包括它的 Data
关联类型是什么。 responseHandler
回调应该能够处理 Dormouse
大小的东西,还是需要能够处理 IceCreamTruck
大小的东西?
如果编译器不知道,它就无法为 responseHandler
设置正确的调用顺序。如果您试图通过为 Dormouse
!
IceCreamTruck
大小的内容,就会发生不好的事情
如果编译器不知道变异必须提供什么类型的 Data
,它就不能从模板中写出 perform
的正确版本。
如果您只将 func getMutation<T: GraphQLMutation>() -> T
的结果交给它,您已经消除了 Data
类型的证据,它不知道 perform
的版本它应该写。
您试图隐藏 Data
的类型,但您还希望编译器创建一个 perform
函数,其中 Data
的类型是已知的。你不能两者都做。
希望这对您有所帮助:
class GraphQLQueryHelper
{
static let shared = GraphQLQueryHelper()
class func performGraphQLQuery<T:GraphQLQuery>(query: T, completion:@escaping(GraphQLSelectionSet) -> ())
{
Network.shared.apollo().fetch(query: query, cachePolicy: .default) { (result) in
switch result
{
case .success(let res):
if let data = res.data
{
completion(data)
}
else if let error = res.errors?.first
{
if let dict = error["extensions"] as? NSDictionary
{
switch dict.value(forKey: "code") as? String ?? "" {
case "invalid-jwt": /*Handle Refresh Token Expired*/
default: /*Handle error*/
break
}
}
else
{
/*Handle error*/
}
}
else
{
/*Handle Network error*/
}
break
case .failure(let error):
/*Handle Network error*/
break
}
}
}
class func peroformGraphQLMutation<T:GraphQLMutation>(mutation: T, completion:@escaping(GraphQLSelectionSet) -> ())
{
Network.shared.apollo().perform(mutation: mutation) { (result) in
switch result
{
case .success(let res):
if let data = res.data
{
completion(data)
}
else if let error = res.errors?.first
{
if let dict = error["extensions"] as? NSDictionary
{
switch dict.value(forKey: "code") as? String ?? "" {
case "invalid-jwt": /*Handle Refresh Token Expired*/
default: /*Handle error*/
break
}
}
else
{
/*Handle error*/
}
}
else
{
/*Handle Network error*/
}
break
case .failure(let error):
/*Handle error*/
break
}
}
}
}