Swift比许多其他语言更安全、更不容易出错的原因之一是它的高级类型系统(从某种程度上说,是不可饶恕的)。它是一种语言特性,有时会让人印象深刻,并使您的工作效率大大提高,但有时又会让人感到非常沮丧。
今天,我想强调在Swift中处理泛型时可能出现的一种情况,以及我通常如何使用基于闭包的类型擦除技术来解决它。
假设我们想要编写一个类,允许我们通过网络加载模型。因为我们不想在我们的应用程序中为每个模型复制这个类,所以我们选择让它成为一个通用类,像这样:
class ModelLoader<T: Unboxable & Requestable> {
func load(completionHandler: (Result<T>) -> Void) {
networkService.loadData(from: T.requestURL) { data in
do {
try completionHandler(.success(unbox(data: data)))
} catch {
let error = ModelLoadingError.unboxingFailed(error)
completionHandler(.error(error))
}
}
}
}
到目前为止,一切都很好,我们现在有了一个ModelLoader,它能够加载任何模型,只要它是Unboxable,并且能够给我们一个requestURL。但是,我们也想让使用这个模型加载器的代码易于测试,所以我们将它的API提取到协议中:
protocol ModelLoading {
associatedtype Model
func load(completionHandler: (Result<Model>) -> Void)
}
这一点,再加上依赖注入,使我们能够在测试中轻松地模拟我们的模型加载API。 但它有一点复杂,在那无论什么时候我们想要使用这个API,我们现在必须将其称为协议ModelLoading,它具有关联的类型需求。这意味着仅仅引用ModelLoading是不够的,因为如果没有更多的信息,编译器就不能推断出它的关联类型。所以试着这样做:
class ViewController: UIViewController {
init(modelLoader: ModelLoading) {
...
}
}
will give us this error:
Protocol 'ModelLoading' can only be used as a generic constraint because it as Self or associated type requirements
但是不用担心,我们可以通过使用一个通用的约束来轻松地消除这个错误,强制符合ModelLoading的具体类型将由API用户指定,并且它将加载我们期望的模型类型。是这样的:
class ViewController: UIViewController {
init<T: ModelLoading>(modelLoader: T) where T.Model == MyModel {
...
}
}
这是可行的,但因为我们还希望在视图控制器中有一个模型加载器的引用,所以我们需要能够指定该属性的类型。 T只在初始化器的上下文中是已知的,所以我们不能用类型T定义属性,除非我们让视图控制器类本身成为泛型 -这将很快使我们陷入到处都是泛型类的兔子洞中。
相反,让我们使用类型擦除,使我们能够保存对T的某种引用,而不实际使用它的类型。 这可以通过创建一个类型擦除的类型来实现,例如一个包装类,如下所示:
class AnyModelLoader<T>: ModelLoading {
typealias CompletionHandler = (Result<T>) -> Void
private let loadingClosure: (CompletionHandler) -> Void
init<L: ModelLoading>(loader: L) where L.Model == T {
loadingClosure = loader.load
}
func load(completionHandler: CompletionHandler) {
loadingClosure(completionHandler)
}
}
以上是一种类型擦除技术,它在Swift标准库中也非常常用,例如在AnySequence类型中。基本上,您可以将具有关联值需求的协议包装到泛型类型中,然后您就可以使用该泛型,而不必一直使使用它的所有内容也都是泛型的。
我们现在可以更新我们的视图控制器,使用AnyModelLoader:
class ViewController: UIViewController {
private let modelLoader: AnyModelLoader<MyModel>
init<T: ModelLoading>(modelLoader: T) where T.Model == MyModel {
self.modelLoader = AnyModelLoader(loader: modelLoader)
super.init(nibName: nil, bundle: nil)
}
}
现在,我们有了一个面向协议的API,具有简单的可模拟性,它仍然可以在非泛型类中使用,这要感谢类型擦除🎉
现在是奖金环节。上面的技术工作得非常好,但是它确实涉及了一个额外的步骤,给我们的代码增加了一些复杂性。但实际上,我们可以直接在视图控制器中做基于闭包的类型擦除 -不必引入AnyModelLoader类。然后,我们的视图控制器就会变成这样:
class ViewController: UIViewController {
private let loadModel: ((Result<MyModel>) -> Void) -> Void
init<T: ModelLoading>(modelLoader: T) where T.Model == MyModel {
loadModel = modelLoader.load
super.init(nibName: nil, bundle: nil)
}
}
与我们的类型删除类AnyModelLoader一样,我们可以将load函数的实现作为一个闭包引用,然后在视图控制器中保存对它的引用。现在,当我们想要加载一个模型时,我们只需调用loadModel,就像我们对其他任何函数或闭包一样:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadModel { result in
switch result {
case .success(let model):
render(model)
case .error(let error):
render(error)
}
}
}
就是这样!希望您在处理Swift代码中的泛型和协议时,会发现上述技术有用