具有最终类的多态性,可在 swift 中实现关联类型协议
Posted
技术标签:
【中文标题】具有最终类的多态性,可在 swift 中实现关联类型协议【英文标题】:Polymorphism with a final class that implements an associatedtype protocol in swift 【发布时间】:2021-12-22 11:03:59 【问题描述】:我正在使用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 服务器端点生成类。它们是这样的:
public final class ConcreteMutation: GraphQLMutation
...
public struct Data: GraphQLSelectionSet
...
...
据我所知(我是 Swift 新手),我无法控制到目前为止我提到的任何代码(除了分叉和修改它)。我可以在本地更改它们,但它们只会在每次重新生成时被覆盖。
要使用这些生成的类中的任何一个,我必须将它们传递给这个ApolloClient
函数(也是一个库类):
@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
...
这个函数在枚举中的事实与我无关:相同的代码可以在结构/类/任何东西中。 重要的是函数签名。我想要一个返回 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)
...
这是很多重复的代码。
取决于我如何摆弄我的getMutation
签名——例如,<T: GraphQLMutation>() -> T?
等——我可以编译 func,但是当我尝试将它传递给 ApolloClient.perform()
时会出现不同的编译错误.有人说“协议只能用作通用约束,因为它具有 Self 或关联的类型要求。”
我对此进行了很多研究,我的研究发现this article,但我认为如果实现关联类型的具体类是最终的,这不是一个选项?
真的很难弄清楚在这种情况下是否可以使用多态性。我可以找到很多关于你可以做什么的文章,但没有关于你不能做什么的文章。我的问题是:
如何编写 getMutation
以便它返回一个可以传递给 ApolloClient.perform()
的值?
【问题讨论】:
对于ConcreteMutation
之一,Data
的类型在哪里建立?它们是否使用定义Data
的类型别名生成
@ScottThompson 我更新了我的问题。它似乎是一个公共结构?我看错了吗?如果您想了解有关该实施的更多信息,请告诉我。
【参考方案1】:
你遇到的根本问题是这个函数签名:
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
的类型是已知的。你不能两者都做。
【讨论】:
有趣。既然不能两者都做,有没有比我现在更好的解决方案:一遍又一遍地复制和粘贴几乎相同的功能? 为什么会有中间函数useConcreteMutation2
?它比apolloClient.perform(mutation: ConcreteMutation2(last_name: value))
为您提供什么好处? (我怀疑如果函数没有占位符名称,值会更明显)。
这是一个很好的问题。我将不得不对此进行调查并回复您,但感谢您迄今为止的帮助。【参考方案2】:
也许你需要在associatedtype
上实现AnyGraphQLMutation
类型擦除。
网上有很多资源(类型擦除),我发现this one 非常详尽。
【讨论】:
感谢您的帮助,但您能告诉我如何使用示例中的代码来实现吗?我在这个问题上遇到的主要挑战是对是否存在解决方案进行肯定/否定。 如果你能明确一点,我会说:SomeClass
是什么,你在那个方法原型中切换了什么。
您的 (?) 协议 GraphQLOperation
是具有关联类型(顺便说一句不好的命名)Data
的协议,我看不到在其属性的任何地方使用它。最后一件事:GraphQLSelectionSet
是什么?
关于你的第一个问题, SomeClass 是一个枚举。我正在切换枚举的元素。关于你的第二个问题,抱歉,那不是我的代码,它是 Apollo 的。这是 GraphQLSelectionSet 的链接:github.com/apollographql/apollo-ios/blob/0.49.0/Sources/Apollo/…
感觉在这种情况下类型擦除无济于事,因为您将丢失所有有助于ApolloClient.perform
确定将什么结果传递回完成处理程序的类型信息。【参考方案3】:
我希望这会有所帮助:
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
【讨论】:
谢谢!解释为什么这会有所帮助将更容易判断它是否有帮助。 ***.com/questions/66194191/…以上是关于具有最终类的多态性,可在 swift 中实现关联类型协议的主要内容,如果未能解决你的问题,请参考以下文章