编程iOS开发者,中级/高级

在Swift中,type erasure是什么,它有什么用,以及如何在实践中正确实现它?

用 Hintsage AI 助手通过面试

答案。

问题历史: 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())]

关键特点:

  • 允许将带有associatedtype的协议作为普通类型使用
  • 解决了通用容器和协议实体工厂的问题
  • 是一种架构模式,用于隐藏实现细节

题外问题。

能否在没有type erasure的情况下,将带有associatedtype的协议用作属性或数组的类型?

不可以。编译器要求具体化所有关联类型。例如,以下语句会产生错误:

let array: [Animal] // 错误:“Animal”只能作为泛型约束使用

type erasure能否替代继承?

不,type erasure并不是对继承的完全替代。它解决了带有associatedtype的协议的抽象问题,而继承用于在类之间实现共享逻辑和代码重用。

是否必须在type erasure包装中实现协议的所有方法和属性?

是的,必须。只有当包装完全重复协议的外部接口时,type erasure才能起作用,否则部分功能将不可用。

常见错误和反模式

  • 忘记实现所有方法/属性,导致功能的silent loss
  • 在可以无需type erasure的地方使用(例如,没有associatedtype时)
  • 创建冗余或过于复杂的包装,降低可读性

生活中的例子

负面案例

在一个实际项目中,所有与数据源的工作都通过type erasure完成,即便没有需要的associatedtype。代码变得难以维护,新开发者感到困惑。

优点:

  • API的通用性

缺点:

  • 可读性降低,架构复杂化,潜在bug

正面案例

Type erasure仅用于对数据源的抽象,该数据源封装了不同的加载策略(网络、本地),每种策略都有自己的类型。在其他方面,代码是透明的。

优点:

  • 架构灵活性,集成新策略的简单性

缺点:

  • 增加了中介层(包装),略微增加了调试的难度