问题历史: Type erasure(类型擦除)是一种模式,作为对与关联类型或Self要求的协议的限制的回应而出现在Swift中。如果协议包含关联类型,编译器就不知道具体的实现,因此不能直接将协议用作类型。在创建数据容器、集合和流时,这通常会遇到问题,抽象是非常重要的。
问题: 如果有一个带有associatedtype的协议,例如:
protocol Animal { associatedtype Food func eat(_ food: Food) }
就不能声明一个Animal类型的变量。我们需要一个“擦除包装”类型,它可以通过抽象来引用不同的具体类型:
let zoo: [any Animal] // 编译错误
解决方案: Type erasure通过包装(例如,Box模式或struct AnyXxx)实现,提供一个统一接口,隐藏实际类型的细节:
struct AnyAnimal<F>: Animal { private let _eat: (F) -> Void init<A: Animal>(_ base: A) where A.Food == F { _eat = base.eat } func eat(_ food: F) { _eat(food) } }
现在可以存储不同实现的Animal,它们具有相同的Food:
let animals: [AnyAnimal<Grass>] = [AnyAnimal(Cow()), AnyAnimal(Sheep())]
关键特点:
能否在没有type erasure的情况下,将带有associatedtype的协议用作属性或数组的类型?
不可以。编译器要求具体化所有关联类型。例如,以下语句会产生错误:
let array: [Animal] // 错误:“Animal”只能作为泛型约束使用
type erasure能否替代继承?
不,type erasure并不是对继承的完全替代。它解决了带有associatedtype的协议的抽象问题,而继承用于在类之间实现共享逻辑和代码重用。
是否必须在type erasure包装中实现协议的所有方法和属性?
是的,必须。只有当包装完全重复协议的外部接口时,type erasure才能起作用,否则部分功能将不可用。
在一个实际项目中,所有与数据源的工作都通过type erasure完成,即便没有需要的associatedtype。代码变得难以维护,新开发者感到困惑。
优点:
缺点:
Type erasure仅用于对数据源的抽象,该数据源封装了不同的加载策略(网络、本地),每种策略都有自己的类型。在其他方面,代码是透明的。
优点:
缺点: