在面向对象的编程中,抽象类型提供了一个基本实现,其他类型可以从中继承一些共享的、通用的功能。抽象类型不同于普通类型,因为它们从不按原样使用(事实上,一些编程语言甚至阻止抽象类型被直接实例化),因为它们的唯一目的是作为一组相关的公共父类类型。例如,假设我们想统一我们通过网络加载某些类型模型的方式,通过提供一个共享的API,我们将能够使用它来分离关注点,使依赖注入[1]和模拟[2]变得容易,并且保持方法名称在我们的项目中保持一致。一种基于抽象类型的方法是使用一个基类作为我们所有模型加载类型的共享、统一的接口。由于我们不希望直接使用此类,因此我们希望它在基类的实现被错误调用时引发fatalError:classLoadable{funcload(fromurl:URL)asyncthrows->Model{fatalError("load(from:)hasnotbeenimplemented")}}然后每个Loadable子类都会重写上面的load方法来提供它的加载功能,如下:如果上面的模式看起来很眼熟,那可能是因为它本质上与我们通常在Swift中使用的协议[3]的多态性。也就是说,当我们要定义一个接口,一个契约的时候,可以通过不同的实现来符合多种类型。classUserLoader:Loadable{overridefuncload(fromurl:URL)asyncthrows->User{...}}不过,协议确实比抽象类有显着优势,因为编译器会强制执行所有要求都得到正确实现-这意味着我们不再需要依赖运行时错误(例如fatalError)来防止不当使用,因为我们无法实例化协议。所以,如果我们采用面向协议的方法,而不是使用抽象基类,我们之前的Loadable和UserLoader类型可能看起来像这样::Loadable{funcload(fromurl:URL)asyncthrows->User{...}}注意我们现在如何使用相关类型让每个Loadable实现决定它想要加载的确切模型-这给了我们一个很好的组合在完整的类型安全性和极大的灵活性之间。所以,一般来说,协议绝对是Swift中声明抽象类型的首选方式,但这并不意味着它们是完美的。事实上,我们基于协议的Loadable实现目前有两个主要缺点:首先,由于我们必须在我们的协议中添加相关类型以保持我们的设计通用和类型安全,这意味着Loadable不能再被直接引用。其次,由于协议不能包含任何形式的存储。如果我们想添加任何存储的属性以供所有Loadable实现使用,我们必须在每个具体实现中重新声明这些属性。相对于我们之前基于抽象类的设计,这个属性存储方面确实是一个巨大的优势。因此,如果我们将Loadable还原为一个类,那么我们可以将我们的子类需要的所有对象直接存储在我们的基类中——无需在多种类型中重复声明这些属性:classLoadable{letnetworking:Networkingletcache:Cacheinit(networking:Networking,cache:Cache){self.networking=networkingself.cache=cache}funcload(fromurl:URL)asyncthrows->模型{fatalError("load(from:)尚未实现")}}classUserLoader:Loadable{overridefuncload(fromurl:URL)asyncthrows->User{ifletcachedUser=cache.value(forKey:url){returncachedUser}letdata=tryawaitnetworking.data(from:url)...}}所以,我们在这里处理的基本上是一个典型的权衡,两种方法(抽象类和协议)都给我们带来了不同的优点和缺点。但是,如果我们可以将这两种方法结合起来并获得两全其美呢?如果我们考虑一下,基于抽象类的方法唯一真正的问题是我们必须在方法中实现fatalError,那么如果我们只为这个特定的方法使用协议怎么办?然后我们仍然可以在基类中保留我们的网络和缓存属性-像这样:init(networking:Networking,cache:Cache){self.networking=networkingself.cache=cache}}但这种方法的主要缺点是所有具体实现现在都必须继承LoadableBase并声明它们符合我们新的LoadableProtocol协议:classUserLoader:LoadableBase,LoadableProtocol{...}这可能不是一个大问题,但可以说它确实使我们的代码不那么优雅。不过,好消息是我们实际上可以通过使用泛型类型别名来解决这个问题。由于Swift的组合运算符&支持组合类和协议,我们可以重新引入我们的Loadable类型作为LoadableBase和LoadableProtocol之间的组合:typealiasLoadable=LoadableBase&LoadableProtocol像这样,具体来说UserLoader之类的类型可以简单地声明它们基于Loadable,编译器将确保所有这些类型都实现我们协议的加载方法——同时仍然允许这些类型使用我们基类中声明的属性:URL)asyncthrows->User{ifletcachedUser=cache.value(forKey:url){returncachedUser}letdata=tryawaitnetworking.data(from:url)...}}伟大的!上述方法唯一真正的缺点是Loadable仍然不能直接引用,因为它仍然是通用协议的一部分。但这实际上可能不是问题——如果是这样,那么我们总是可以使用类型擦除等技术来修复它们。Loadable的基于别名的新型设计的另一个小警告是,无法扩展此类复合类型别名,如果我们想提供一些我们不想(或不能)直接实现的便利API,则可能会发生这种情况LoadableBase类将是一个问题。不过,解决这个问题的一种方法是在我们的协议中声明我们实现这些便利API所需的一切,这将允许我们自己扩展协议:,Model>{get}funcload(fromurl:URL)asyncthrows->Model}extensionLoadableProtocol{funcloadWithCaching(fromurl:URL)asyncthrows->模型{ifletcachedModel=cache.value(forKey:url){returncachedModel}letmodel=tryawaitload(from:url)cache.insert(model,forKey:url)returnmodel}}}这些是在Swift中使用抽象类型和方法的几种不同方式。子类化可能不像以前那样流行(在其他编程语言中也是如此),但我仍然认为这些技术在我们整体的Swift开发工具箱中非常有用。参考资料[1]依赖注入:https://www.swiftbysundell.com/articles/different-flavors-of-dependency-injection-in-swift。2]模拟:https://www.swiftbysundell.com/articles/mocking-in-swift。[3]Swift常用协议:https://www.swiftbysundell.com/basics/protocols。