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代码中的泛型和协议时,会发现上述技术有用

原文链接